ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python全栈股票分析系统:从数据采集到回测的完整搭建指南

Python全栈股票分析系统:从数据采集到回测的完整搭建指南 简介这是一套基于Python构建的全栈股票分析系统源码面向金融数据分析学习者、量化研究入门者及需要搭建个性化行情工具的开发者。系统以akshare为核心数据接口覆盖A股、港股、美股等多市场行情并整合pandas、TensorFlow、tornado等工具实现数据采集、统计建模与可视化展示的完整链路同时支持实时追踪与历史回溯。资源包共172个文件约2.18MB包含29个py核心脚本、34个js与19个html/css前端页面、28个zbak备份文件以及dockerfile、conf、sh等部署配置目录结构清晰便于按模块阅读与二次开发。已有47人学习下载。代码经过重构模块化程度较高pandas强化了时间序列清洗能力tornado异步架构支撑高并发实时更新TensorFlow用于预测与风险评估建模适合作为金融分析工具链的实践参考。1. 一套能跑起来的 Python 全栈股票分析系统到底由哪些模块拼成很多人搜「基于Python的全栈股票分析系统源码」脑子里想的是下载一个压缩包、解压、python app.py然后浏览器里就出现一个能看 K 线、能选股、能回测的完整系统。现实是这类项目从来不是「一个文件跑天下」而是数据采集、数据存储、指标计算、策略回测、Web 接口、前端展示这几块拼起来的。全栈两个字重点不在「全」而在「栈」——每一层都要能独立跑通再串成一条链路。这套系统适合两类人一类是刚学完 Python 基础语法、想找一个真实项目练手的开发者另一类是手里有选股想法、但缺一套可复用分析框架的从业者。它解决的核心问题是把「拉数据 → 算指标 → 出信号 → 看结果」这条链路固化下来而不是每次都在 Jupyter 里重新写一遍。下面按模块拆开讲每一步都落到能复现的命令和代码上。2. 数据层怎么搭从行情接口到本地缓存的完整链路2.1 为什么先定数据源再写任何分析代码股票分析系统的第一道坎不是算法是数据。常见做法是先用公开行情接口拉日线数据落到本地做缓存后续所有指标计算和回测都读本地文件。这样做的好处是接口挂了不影响你调试策略回测速度也快一个量级。我一般会选akshare或tushare这类库做数据入口前者免费、覆盖 A 股较全后者需要 token 但字段规范。选型时看三个点一是复权处理是否内置二是历史数据能拉多长三是请求频率限制。日线级别分析前两点比第三点重要因为日线数据一天只需要更新一次频率压力不大。把数据源定下来之后目录结构就要提前规划否则后面指标、回测、接口三层会互相污染。# 项目根目录结构先建好再写代码 stock-analysis/ ├── data/ # 本地行情缓存按股票代码分文件 ├── indicators/ # 指标计算模块 ├── backtest/ # 回测引擎 ├── api/ # Web 接口层 ├── web/ # 前端页面 └── config.py # 数据源、路径、参数集中配置这个结构的关键是data/和config.py分离。数据文件不进版本控制配置集中管理换数据源时只改config.py一个地方。2.2 用 akshare 拉日线并落地成 parquet 的最小脚本下面这段代码做三件事拉一只股票的日线、做前复权、存成 parquet。parquet 比 CSV 快列式存储对指标计算友好文件体积也小。import akshare as ak import pandas as pd from pathlib import Path DATA_DIR Path(data) DATA_DIR.mkdir(exist_okTrue) def fetch_daily(symbol: str, start: str, end: str) - pd.DataFrame: # symbol 格式如 000001akshare 需要不带交易所前缀 df ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart, end_dateend, adjustqfq # 前复权回测必须用复权价 ) # 统一列名避免后续模块依赖中文列名 df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume }) df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) return df def save_daily(symbol: str, df: pd.DataFrame): path DATA_DIR / f{symbol}.parquet df.to_parquet(path, indexFalse) print(fsaved {len(df)} rows - {path}) if __name__ __main__: df fetch_daily(000001, 20200101, 20241231) save_daily(000001, df)逻辑说明adjustqfq是回测正确性的前提用不复权价算出来的收益率是错的。列名统一成英文是为了后面指标模块不依赖中文列名换数据源时只改这一个函数。参数上start_date和end_date用字符串传入akshare 内部会解析不用自己转 datetime。2.3 增量更新与数据校验的两个必做检查全量拉一次之后日常只需要增量更新。增量逻辑是读本地 parquet 的最后一条日期从那天往后拉。这里有两个坑一是停牌日会导致日期不连续二是接口偶尔返回空 DataFrame。所以更新函数里必须做校验。def update_daily(symbol: str): path DATA_DIR / f{symbol}.parquet if not path.exists(): df fetch_daily(symbol, 20200101, 20241231) save_daily(symbol, df) return old pd.read_parquet(path) last_date old[date].max().strftime(%Y%m%d) new fetch_daily(symbol, last_date, 20241231) if new.empty: print(f{symbol} no new data) return # 去掉重复日期避免接口把最后一天重复返回 merged pd.concat([old, new]).drop_duplicates(date).sort_values(date) save_daily(symbol, merged)校验点一drop_duplicates(date)必须做否则重复行会让指标计算出现跳变。校验点二合并后检查merged[date].is_monotonic_increasing如果不是递增说明排序有问题回测会错乱。这两步看起来简单但漏掉任何一个后面排查起来就是黑匣子。3. 指标与回测层把选股想法变成可验证的数字3.1 均线、MACD、RSI 三个指标的向量化写法指标计算最忌讳用 for 循环逐行算pandas 的向量化写法快几十倍。下面把三个常用指标写成独立函数输入统一是带close列的 DataFrame输出新增指标列。import pandas as pd import numpy as np def add_ma(df: pd.DataFrame, windows(5, 20, 60)) - pd.DataFrame: for w in windows: df[fma{w}] df[close].rolling(w).mean() return df def add_macd(df: pd.DataFrame, fast12, slow26, signal9) - pd.DataFrame: ema_fast df[close].ewm(spanfast, adjustFalse).mean() ema_slow df[close].ewm(spanslow, adjustFalse).mean() df[dif] ema_fast - ema_slow df[dea] df[dif].ewm(spansignal, adjustFalse).mean() df[macd] (df[dif] - df[dea]) * 2 return df def add_rsi(df: pd.DataFrame, period14) - pd.DataFrame: delta df[close].diff() gain delta.clip(lower0).rolling(period).mean() loss (-delta.clip(upper0)).rolling(period).mean() rs gain / loss.replace(0, np.nan) df[rsi] 100 - 100 / (1 rs) return df参数说明均线窗口 5/20/60 是常见短中长组合改窗口只影响信号敏感度不影响计算逻辑。MACD 的adjustFalse必须加否则 ewm 会按整个序列长度做调整结果和通达信对不上。RSI 里loss.replace(0, np.nan)是防止除零停牌或一字板时会出现这种情况。3.2 一个最小回测引擎信号、持仓、收益三段式回测引擎不需要复杂核心就三件事根据指标生成买卖信号、按信号模拟持仓、算累计收益。下面是一个单标的、全仓进出的最小版本。def backtest(df: pd.DataFrame, signal_col: str signal) - pd.DataFrame: df df.copy() df[position] df[signal_col].shift(1).fillna(0) # 次日开盘执行 df[ret] df[close].pct_change().fillna(0) df[strategy_ret] df[position] * df[ret] df[equity] (1 df[strategy_ret]).cumprod() return df def make_signal(df: pd.DataFrame) - pd.DataFrame: # 金叉买入死叉卖出 df[signal] 0 df.loc[df[ma5] df[ma20], signal] 1 df.loc[df[ma5] df[ma20], signal] 0 return df逻辑说明shift(1)是关键信号当天收盘后才知道只能次日执行不加这一行就是未来函数回测收益会虚高。position用 0/1 表示空仓满仓想加仓位管理就把这里改成 0 到 1 之间的浮点数。equity是净值曲线最后拿它算最大回撤和年化。3.3 回测结果里必须看的三个数字跑完回测别只看总收益。我一般先看三个数年化收益、最大回撤、夏普比率。年化收益高但回撤 60% 的策略实盘拿不住。夏普低于 1 的策略说明收益是靠承担波动换来的性价比不高。def metrics(df: pd.DataFrame) - dict: total df[equity].iloc[-1] - 1 days (df[date].iloc[-1] - df[date].iloc[0]).days annual (1 total) ** (365 / days) - 1 drawdown (df[equity] / df[equity].cummax() - 1).min() sharpe df[strategy_ret].mean() / df[strategy_ret].std() * (252 ** 0.5) return {annual: annual, max_drawdown: drawdown, sharpe: sharpe}参数上252 是 A 股一年交易日数用 365 算夏普会偏低。最大回撤用cummax算逻辑是当前净值相对历史最高点的跌幅。这三个数字出来策略值不值得继续调心里就有数了。4. 接口与前端让分析结果能被浏览器访问4.1 用 FastAPI 暴露三个核心接口后端接口不用多三个就够股票列表、单只股票指标数据、回测结果。FastAPI 自带文档调试方便。from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import pandas as pd app FastAPI() app.add_middleware(CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*]) app.get(/api/stock/{symbol}) def get_stock(symbol: str): df pd.read_parquet(fdata/{symbol}.parquet) df add_ma(add_macd(add_rsi(df))) return df.tail(250).to_dict(orientrecords) app.get(/api/backtest/{symbol}) def get_backtest(symbol: str): df pd.read_parquet(fdata/{symbol}.parquet) df make_signal(add_ma(df)) df backtest(df) return metrics(df)逻辑说明CORS中间件必须加否则前端页面跨域请求会被浏览器拦。tail(250)只返回最近一年数据前端画图够用全量返回会让接口变慢。参数上symbol直接拼进文件路径生产环境要做白名单校验防止路径穿越。4.2 前端用 ECharts 画 K 线和指标的最小页面前端不需要框架也能跑一个 HTML 加 ECharts CDN 就够。核心是把接口返回的数组喂给 ECharts 的 candlestick 和 line。!DOCTYPE html html head script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idchart stylewidth:100%;height:600px;/div script fetch(http://localhost:8000/api/stock/000001) .then(r r.json()) .then(data { const dates data.map(d d.date); const kline data.map(d [d.open, d.close, d.low, d.high]); const ma5 data.map(d d.ma5); const chart echarts.init(document.getElementById(chart)); chart.setOption({ xAxis: { type: category, data: dates }, yAxis: { scale: true }, series: [ { type: candlestick, data: kline }, { type: line, data: ma5, smooth: true } ] }); }); /script /body /html逻辑说明ECharts 的 candlestick 数据顺序是[open, close, low, high]顺序错了图会乱。scale: true让 Y 轴不强制从 0 开始K 线形态才看得清。前端页面直接双击打开即可接口地址写死 localhost部署时改成实际域名。5. 避坑与排查这套系统最容易翻车的五个地方5.1 复权没做对回测收益全是假的现象回测年化 80%实盘一跑就亏。原因用了不复权价除权除息日价格跳空被当成真实涨跌。解决数据层统一用adjustqfq并且回测和指标计算读同一份复权数据不要一个用复权一个用不复权。5.2 未来函数藏在 shift 里现象策略胜率 90%但信号出现当天就买入。原因signal没做shift(1)用了当天收盘价生成的信号去算当天收益。解决所有信号列在进入回测前统一shift(1)并且检查position列的第一行是不是 0。5.3 停牌日导致指标断层现象某只股票均线突然跳变。原因停牌期间没有行情数据rolling 窗口跨过了停牌日。解决拉数据时用交易日历对齐停牌日填充前收盘价或者直接在指标计算前dropna掉停牌区间。5.4 接口返回中文列名导致前端报错现象前端拿到的数据里 key 是「日期」「收盘」ECharts 取不到值。原因数据层没统一列名直接透传了原始中文列。解决在数据层rename成英文接口层只返回英文 key前端按英文 key 取值。5.5 parquet 文件被并发写入损坏现象更新数据时程序崩溃parquet 文件读不出来。原因多个进程同时写同一个文件。解决更新任务串行执行或者先写临时文件再os.replace原子替换。我一般会在更新函数里加文件锁避免定时任务和手动更新撞车。6. 进阶技巧用参数网格快速验证策略稳健性单跑一组参数说明不了问题真正要看的是策略在不同参数下的表现是否稳定。我一般会做参数网格把均线窗口、MACD 参数、RSI 阈值组合起来跑一遍看收益和回撤的分布。import itertools def grid_search(df: pd.DataFrame, ma_windows, rsi_periods): results [] for ma, rsi in itertools.product(ma_windows, rsi_periods): d add_ma(df.copy(), windows(ma, ma * 4)) d add_rsi(d, periodrsi) d[signal] (d[fma{ma}] d[fma{ma*4}]).astype(int) d backtest(d) m metrics(d) results.append({ma: ma, rsi: rsi, **m}) return pd.DataFrame(results).sort_values(sharpe, ascendingFalse)参数说明ma_windows传[5, 10, 20]rsi_periods传[6, 14, 24]组合出 9 组结果。看sharpe排序如果前几名参数差异很大但夏普都高于 1说明策略稳健如果只有某一组特别高大概率是过拟合。这一步跑完再决定要不要上实盘模拟。我自己的习惯是任何策略在网格搜索里夏普波动超过 50%就直接放弃不浪费时间去调。这套系统搭起来大概两天但省下的是后面无数次重复造轮子的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表