简介:这是一份基于Python的股票自动交易系统毕业设计项目源码,面向计算机、软件工程等专业学生与开发者,适用于毕业设计、期末大作业、课程设计等场景。项目历经完整调试与测试,答辩评审分达95分。系统涵盖行情数据获取、交易策略编写、自动下单执行及SQL数据存储等核心模块。压缩包共686个文件、14.77MB,主体为Python策略与调度代码、131个Java业务模块、HTML/CSS/JS前端页面、XML配置文件及近300张PNG/JPG界面截图,另附Dockerfile与SQLite数据库文件。目前已有337人学习下载,下载后可直接运行项目工程,获得完整目录结构、数据库脚本与配置说明,基础扎实的读者还可在此基础上扩展交易策略或接入实盘接口,实现个性化功能改造。
1. 股票自动交易系统到底是什么:一个毕设源码包里的完整闭环
拿到“基于Python的股票自动交易系统的设计与实现”这个题目时,很多人第一反应是“让程序自己买卖股票、躺着赚钱”。实际打开这类毕设源码包你会发现,它讲的是另一件事:把一套完整的交易闭环拆成数据获取、策略信号、订单执行、风险控制、回测验证五个模块,用 Python 串起来。真正值钱的部分不是“自动下单”那一步,而是“信号怎么算、仓位怎么控、回测怎么不骗人”。这套系统适合两类人:一类是要交毕设的学生,需要快速搞懂每行代码对应哪个交易环节;另一类是刚接触量化、想用最小代价验证一个策略想法的开发者。它解决的核心问题是:在不上真金白银的前提下,把“如果按这个规则交易会怎样”这个黑匣子打开。
2. 拆解源码包:一个自动交易系统的模块划分与选型理由
拿到 zip 先别急着跑,先看目录。绝大多数股票自动交易系统的源码包都长成一个样子:一个 main.py 做启动入口,一个 config.py 放所有可调参数,剩下按职责拆成 data、strategy、trader、risk、backtest 五个包。能分清这五块,后面改代码、写论文、答辩都有抓手。
2.1 五块核心模块:数据、策略、执行、风控、回测各自干什么
第一块是数据模块。它负责拉取历史 K 线和实时行情,清洗后落库。常见做法是每天收盘后跑一次增量更新,把当日 OHLCV(开高低收量)追加到 SQLite 或 CSV 文件里。数据模块做得差,后面所有信号都是垃圾进垃圾出,所以源码里这一块通常写着最多的重试和异常处理。
第二块是策略模块。它接收 DataFrame 格式的行情数据,经过指标计算后输出一个信号:买、卖、持有。毕业设计里最常见的策略是双均线金叉死叉、RSI 超买超卖、布林带突破,因为它们逻辑简单、画图直观、答辩时三句话能说清楚。
第三块是执行模块。它的职责是把“买”这个信号翻译成具体订单:买多少股、按什么价格、什么时间提交。毕设源码里这里通常是一个订单状态机,维护等待、已提交、已成交、被拒绝几个状态。
第四块是风控模块,也是评阅老师最喜欢追问的部分。它的典型工作是三件事:单笔仓位不超过总资金的一定比例、一天内下单次数设上限、单笔亏损超过阈值就强制平仓。没有风控模块的系统只能叫“策略演示”,不配叫“交易系统”。
第五块是回测模块。它把历史数据喂给策略,模拟按历史价格成交,最后算出净值曲线、年化收益、最大回撤。注意,回测模块最容易造假,很多毕设源码里回测收益高得离谱,原因后面第 5 章细说。
2.2 为什么 Python 能扛住这套系统:三点选型支撑
选 Python 不是因为它快,而是因为它让“策略想法到验证结果”的距离最短。第一,pandas 的 rolling、shift、groupby 天生就是为 K 线计算设计的,写一个双均线策略只要十行以内,换成 C++ 光是处理数据结构就要半天。第二,数据获取有现成的开源接口,a 股日线数据可以直接通过公共数据接口拉取,不需要自己维护爬虫。第三,Python 的调度生态成熟,APScheduler 加两行配置就能实现“每个交易日收盘后自动跑任务”,不需要额外部署队列系统。
有些同学问过能不能用 Node.js 或者 Go 写。可以,但你要自己处理 DataFrame 缺失值、时间序列对齐、复权换算,这些在 Python 里都是已经解决的问题。做毕设的核心是快速出结果,不是重新发明轮子。
2.3 最小工程骨架:先按这份目录把代码立起来
我一般建议拿到任何源码包后,先重建一份最小骨架,确认每个文件职责单一,再往里面填逻辑。下面这份目录结构是这类毕设源码最常见的形式,你可以拿它跟手里的 zip 对比:
stock_auto_trader/ ├── config.py # 全局配置:股票代码、策略参数、风控参数 ├── main.py # 启动入口,注册定时任务 ├── data_fetcher.py # 行情数据获取与本地落库 ├── strategy/ │ ├── __init__.py │ ├── base.py # 策略基类,规定输入输出接口 │ └── dual_ma.py # 双均线策略 ├── trader/ │ ├── __init__.py │ └── order.py # 订单状态机 ├── risk/ │ └── risk_manager.py # 仓位与频次风控 └── backtest/ ├── __init__.py └── engine.py # 回测撮合引擎main.py 是这个系统的心脏,它决定了每天什么时候干活。下面是一份最小启动入口:
from apscheduler.schedulers.blocking import BlockingScheduler from data_fetcher import fetch_and_store_daily from strategy.dual_ma import DualMaStrategy from trader.order import OrderManager def job(): # 1. 拉取最新行情并追加到本地库 df = fetch_and_store_daily(code="600000") # 2. 用最新数据生成策略信号 strategy = DualMaStrategy(short_window=5, long_window=20) signal = strategy.generate_signal(df) # 3. 把信号交给订单管理器去处理 order_manager = OrderManager() order_manager.exec_if_needed(signal) if __name__ == "__main__": scheduler = BlockingScheduler() # 周一至周五 15:05 执行,A股已收盘,当日K线定型 scheduler.add_job(job, "cron", day_of_week="mon-fri", hour=15, minute=5) scheduler.start()这段代码要讲清楚两点。第一,为什么选 15:05 而不是盘中跑:日线策略依赖“当日收盘价”这个确定值,盘中跑时最后一根 K 线还在变动,信号会跳来跳去。第二,BlockingScheduler 会阻塞主线程,所以这个进程必须常驻运行,适合部署在实验室或家里的普通电脑上。
config.py 里通常会这样组织参数:
# config.py CODE = "600000" # 目标股票代码 SHORT_WINDOW = 5 # 双均线短周期 LONG_WINDOW = 20 # 双均线长周期 MAX_POSITION_RATIO = 0.3 # 单票最大仓位占总资金比例 MAX_TRADES_PER_DAY = 1 # 单日最大下单次数 SLIPPAGE = 0.001 # 滑点比例,回测时使用 COMMISSION = 0.0003 # 佣金比例,双边收取把所有魔法数字集中到 config.py,是这类源码包最值得学习的设计习惯。答辩时老师问“这个 5 和 20 是什么意思”,你只需要指着配置文件说这是短中两条均线的周期,可调。
3. 策略信号是怎么算出来的:双均线与订单状态机
策略模块是整套系统里最像“玄学”的部分,因为同样的均线参数,换一只股票、换一段行情,结果可能完全相反。但源码实现本身并不玄,它就是一个确定的函数:输入历史 DataFrame,输出一个字符串信号。下面拆开看。
3.1 双均线策略的源码实现与未来函数边界
双均线的逻辑一句话:短期均线上穿长期均线时买入,下穿时卖出。下面是去掉多余装饰后的核心实现:
import pandas as pd class DualMaStrategy: def __init__(self, short_window=5, long_window=20): self.short_window = short_window self.long_window = long_window def generate_signal(self, df: pd.DataFrame) -> str: # 关键:用 shift(1) 取前一根K线的收盘价,避免未来函数 close = df["close"].shift(1) short_ma = close.rolling(self.short_window).mean() long_ma = close.rolling(self.long_window).mean() diff = short_ma - long_ma cross_up = (diff > 0) & (diff.shift(1) <= 0) cross_down = (diff < 0) & (diff.shift(1) >= 0) if cross_up.iloc[-1]: return "buy" if cross_down.iloc[-1]: return "sell" return "hold"这里最值得讲的是shift(1)这一行。很多初写策略的同学会直接拿当天的close算均线,然后当天就按这个信号买入,这在实盘里是不可能的——收盘价走完的那一刻,当天交易已经结束了。shift(1)的含义是:只用昨天及以前的数据决定今天的动作,这是策略代码正确性的最低底线,如果源码里没有这一步,回测收益再高都是假的。
参数方面,短窗常用 3 到 10,长窗常用 20 到 60。短窗越小,信号越频繁,交易成本越高;长窗越大,信号越迟钝,趋势启动后进场晚。毕业设计里最稳妥的组合是 5 和 20,因为它是均线策略的经典默认值,论文里也好解释。
3.2 订单状态机:把信号变成不重复发单的可执行订单
策略输出“buy”之后,不能直接下单。你还需要回答三个问题:当前是不是已经持有?今天是不是已经发过单?账户里有没有足够资金?这三个问题由一个订单管理器统一回答,源码里通常长这样:
from enum import Enum from datetime import datetime class OrderStatus(Enum): PENDING = "pending" # 等待条件确认 SUBMITTED = "submitted" # 已提交到模拟撮合 FILLED = "filled" # 已成交 REJECTED = "rejected" # 被风控拒绝 class OrderManager: def __init__(self, init_cash=100000): self.cash = init_cash self.position = 0 self.last_order_date = None self.status = OrderStatus.PENDING def exec_if_needed(self, signal: str) -> str: today = datetime.now().strftime("%Y-%m-%d") if signal == "hold": return "no_order" # 同一天不允许重复发单,防止信号反复触发 if self.last_order_date == today: print("同日重复信号,跳过") return "skipped" if signal == "buy" and self.position > 0: print("已有持仓,跳过买入") return "skipped" if signal == "sell" and self.position == 0: print("空仓状态,跳过卖出") return "skipped" self.last_order_date = today self.status = OrderStatus.SUBMITTED print(f"{today} 提交 {signal} 订单") return f"submitted_{signal}"状态机要解决的核心问题是重复发单。双均线金叉出现后,如果接下来几天 short_ma 一直在 long_ma 上方,策略会连续输出多天“buy”,没有这个状态机就会连续开仓,仓位迅速失控。这里的last_order_date就是一把锁:同一个交易日只认第一个信号。
init_cash和position两个字段维护了账户状态,这个设计虽然简单,但已经构成了一个可扩展的仓位模型。想改成按比例买入,只需要在订单提交前计算buy_amount = self.cash * ratio / price,然后把结果传给模拟撮合模块。
3.3 核心参数怎么定:短窗、长窗、仓位与止损
参数不是一拍脑袋定的。毕业设计里最常用的方法是网格扫描:固定长窗,遍历短窗;固定短窗,遍历长窗,然后把每组参数的回测结果拉成一张表,挑一组回撤可控且收益合理的。
仓位上限建议设在总资金的 20% 到 30% 之间。满仓单票是这类系统最常见的翻车原因,因为 A 股个股跌停时卖不出去,满仓意味着没有补救余地。
止损参数建议以固定比例实现,比如亏损达到买入价的 5% 就强平。注意止损逻辑要放在风控模块里,不要和策略信号混在一起。策略负责“什么时候买”,风控负责“什么时候必须走”,两个模块职责分离,代码才不会改一处崩一处。
4. 把股票自动交易系统在本地跑起来:环境、数据与启动时序
理论讲完,现在进入实操。很多同学卡在第一步:源码拿到手,装了一堆依赖,但跑起来就报错。这一章给出一条经过验证的最短路径,从创建虚拟环境到看到第一条日志,全程半小时内完成。
4.1 环境准备与依赖清单
Python 版本建议 3.9 到 3.11,太新或太旧都可能遇到某些库没有预编译包的问题。先创建虚拟环境:
python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate依赖就装五个核心库,装多了反而容易冲突:
pip install pandas apscheduler akshare sqlalchemy如果你手里的源码用了 tushare,就把 akshare 换成 tushare,其余不变。pandas 负责数据处理,apscheduler 负责定时调度,akshare 负责拉行情数据,sqlalchemy 负责把 DataFrame 写进 SQLite。
装好后验证环境:
python -c "import pandas, apscheduler, akshare; print('env ok')"出现env ok说明环境没问题。如果 akshare 安装失败,常见原因是网络超时,可以换国内镜像源再装一次。
4.2 接入行情数据并落库
数据是整套系统的地基。下面这段代码演示如何拉取一只股票的历史日线并写入本地 SQLite:
import akshare as ak import pandas as pd from sqlalchemy import create_engine def fetch_and_store_daily(code: str, start_date: str = "20200101"): # 拉取不复权的日线数据 df = ak.stock_zh_a_hist( symbol=code, period="daily", start_date=start_date, end_date="20250101", adjust="qfq", # qfq 表示前复权,回测必须用前复权 ) # 统一列名,方便后续策略代码调用 df.columns = [ "date", "open", "close", "high", "low", "volume", "amount", "amplitude", "pct_change", "change", "turnover" ] df["date"] = pd.to_datetime(df["date"]) # 写入 SQLite,如果表已存在则追加 engine = create_engine("sqlite:///market.db") df.to_sql("daily_quote", engine, if_exists="append", index=False) print(f"{code} 数据已更新,最新日期 {df['date'].iloc[-1]}") return df这里有两个参数必须说明。symbol传的 6 位数字代码,比如"600000",不需要带交易所前缀。adjust="qfq"是前复权,它把历史价格按分红送股做了修正,保证均线计算不会被除权跳空误导。如果这里改成""即不复权,遇到股票分红时 K 线会出现向下跳空,双均线策略会错误触发卖出信号。
代码里还有一处值得注意:if_exists="append"表示追加写入,但日常使用中重复跑会写入重复数据。改进办法是每次全量拉取后先清空表再写,或者用date + code做唯一索引去重。毕设阶段用追加模式问题不大,但要在论文里写明数据更新的幂等性如何保证。
4.3 启动调度与验证系统是否跑通
数据通了之后,把第 2 章的 main.py 完整跑起来。第一次建议不要等定时任务触发,手动执行一次主流程:
python -c " from data_fetcher import fetch_and_store_daily from strategy.dual_ma import DualMaStrategy from trader.order import OrderManager df = fetch_and_store_daily('600000') signal = DualMaStrategy(5, 20).generate_signal(df) OrderManager().exec_if_needed(signal) "三条日志是跑通标志:第一行出现“数据已更新”,第二行没有报错返回,第三行打印出“提交 x 订单”或“无操作”。然后查一下数据库里的数据行数:
python -c " import pandas as pd from sqlalchemy import create_engine engine = create_engine('sqlite:///market.db') df = pd.read_sql('SELECT COUNT(*), MAX(date) FROM daily_quote', engine) print(df) "能看到统计结果说明数据落库成功。最后再把 main.py 的调度器启动,观察是否在设定的时间点触发任务。注意一点:如果你的电脑在 15:05 正好休眠或断网,任务会错过,APScheduler 默认不补跑。毕设演示时可以手动触发一次任务,效果等同。
5. 自动交易系统的五个常见坑与排查记录
这一章是血泪经验汇总。下面五个坑,每一个我都见过对应版本的源码翻车现场。按“现象 → 原因 → 解决”的顺序写,方便你排查时对照。
5.1 回测曲线漂亮但模拟盘变形:滑点和手续费被忽略了
现象:回测年化 35%,最大回撤 8%,看起来很完美。但把同一策略放到最近三个月的行情里模拟跑,收益明显变差。
原因:源码的回测引擎按收盘价直接成交,没有建模手续费和滑点。A 股双边佣金加印花税合计大约千分之二到千分之三,加上买卖价差,一笔交易的实际成本在 0.3% 到 0.5% 之间。双均线这种高频换手的策略,一年交易几十次,成本累积起来会吃掉大部分收益。
解决:在回测引擎里加上成本模型。买入价 = 信号日次日开盘价 × (1 + 滑点比例),卖出同理;每笔成交扣佣金比例。参数在 config.py 里已经预留,回测时把SLIPPAGE=0.001、COMMISSION=0.0003带入成交价计算即。
5.2 均线信号闪烁:当日K线未走完导致的未来函数
现象:策略在盘中每隔一分钟跑一次,买入信号出现后十分钟又变成持有,过半小时又变成买入,日志里同一日期出现多次信号。
原因:盘中最后一根 K 线没有走完,收盘价还在跳动,均线值随之变化,金叉死叉来回穿越。这是典型的未来函数变体——用了“当前不确定的价格”来决定是否交易。
解决:两个方案任选。一是把系统改成每日收盘后运行,只在 15:00 之后取数据计算,这是日线策略的标准做法;二是在策略函数里丢弃 DataFrame 最后一行,只信任已经收盘的 K 线。改完后再看日志,同一个交易日最多出现一次信号变化。
5.3 非交易时段启动任务,第一笔信号是空的
现象:周一早上 8:00 启动系统,日志打印“数据已更新”,但策略输出 hold,一整天都没有交易。检查数据库发现当天根本没有新数据写入。
原因:A 股周末和节假日不交易,数据源的日线接口不会生成新记录。系统按工作日调度启动,但当天可能不是交易日,或者当天数据源接口尚未更新。
解决:在 job 函数里加交易日判断。数据源本身提供交易日历接口,直接查询当天是否交易;如果非交易日,直接 return,不打日志不跑策略。另外把调度时间设到 15:05 之后,避开数据源更新时间。还需要在任务开头判断“最新数据日期是否等于今天”,防止节假日误跑。
5.4 行情接口限频导致数据中断
现象:系统连续运行几天后,某天日志出现大量连接超时或返回空 DataFrame,之后策略一直输出 hold,但没有人发现。
原因:免费的公共数据接口通常有频率限制,短时间调用次数太多会被临时封禁。日线接口虽然调用频率低,但配合回测批量拉取几百只股票时很容易触发限制。
解决:加三层防护。第一,数据拉取函数里设置重试机制,失败后等待 3 秒重试,最多重试 3 次。第二,把历史数据落库后,增量更新只拉最近几个交易日,而不是每次全量拉取。第三,回测批量使用时在循环里加time.sleep(1)控制频率。这三层加上后,接口中断的概率会大幅下降。
5.5 同一天重复开仓,仓位失控
现象:账户里有 10 万资金,某天日志里出现五笔买单,每笔都按三成仓位买入,结果五笔加起来超过了总资金。
原因:策略每天输出信号后,订单管理器没有按日期去重。金叉出现后连续几天信号都是 buy,如果last_order_date没有正确更新或进程重启后状态丢失,就会重复开仓。
解决:把订单管理器的状态落盘。常见做法是在本地维护一个order_log.csv,每次提交订单先读文件检查今天是否已经下过单,然后追加写入。另外在风控模块加一条硬约束:总持仓市值不得超过总资金的MAX_POSITION_RATIO,超了就拒绝新订单并告警。这两条同时生效,能兜住绝大多数重复开仓问题。
6. 从毕设到可验证:仿真撮合与参数扫描两个进阶点
如果你的答辩老师问“这个系统真的能交易吗”,你不需要展示实盘,但要展示一个让结果可信的仿真环境。这里有两个进阶点,成本低、效果好。
6.1 一个能说服导师的仿真撮合器
回测引擎里最容易被质疑的就是成交价格。真实交易中你不可能刚好买在收盘价,所以仿真撮合要手动加上成交约束。一个能说服人的撮合逻辑是:信号生成于 T 日收盘后,成交于 T+1 日开盘,按 T+1 日开盘价加滑点计算:
def simulate_fill(next_bar: pd.Series, order_side: str, slippage: float = 0.001): """ 按次日开盘价撮合,买入加滑点,卖出减滑点 """ if order_side == "buy": fill_price = next_bar["open"] * (1 + slippage) elif order_side == "sell": fill_price = next_bar["open"] * (1 - slippage) else: return None # 加上双边佣金 commission = fill_price * 1000 * 0.0003 return fill_price * 1000 + commission这段代码的核心是“次日成交”,它彻底切断了当日信号与当日成交之间的未来函数联系。回测结果可能变差,但变得可信,答辩时老师的下一个问题你就已经回答完了。
6.2 参数扫描:找出稳健区间而不是单点最优
手动试参数是玄学,参数扫描是工程。把短窗从 3 到 20、长窗从 20 到 60 各取几个代表值,两两组合回测,最后画出收益和回撤的二维表:
import itertools results = [] for short_win, long_win in itertools.product([3, 5, 10, 20], [20, 30, 60]): strategy = DualMaStrategy(short_win, long_win) stats = run_backtest(strategy, df) # run_backtest 返回收益和回撤 results.append({ "short": short_win, "long": long_win, "annual_return": stats["annual_return"], "max_drawdown": stats["max_drawdown"] }) results_df = pd.DataFrame(results) print(results_df.sort_values("annual_return", ascending=False))看结果时关注两个点:一是收益最高的参数往往伴随最大的回撤,不要只挑收益最高;二是一段参数区间内结果相近时,选中间值,说明策略对参数不敏感,稳定性更好。我看过很多同学挑出收益最高的那组参数,结果换一段行情就失效,真正的稳健区间比单点最优值重要得多。
最后说说我的个人习惯:每做完一次参数调整,我会把回测净值曲线和基准指数叠在一张图上,先看这条曲线有没有长期向上的趋势,再看下跌段的坡度是否在自己能承受的范围。只看最终收益数字是容易骗自己的。希望这一整套从源码拆解到跑通再到避坑的路径,能帮你把这个项目真正变成自己的东西,也希望帮到你。
本文还有配套的精品资源,点击获取