☰
Python爬虫与量化交易:从数据采集到策略回测的完整实践
2026/9/30 4:37:32 网站建设 项目流程

开盘前半小时,我通常会做一件事:打开自己写的那套Python脚本,让它自动拉取持仓和自选股的行情,算完均线、RSI、MACD这些指标,再根据预定义的规则给出"今日关注"清单。这些动作从2019年开始就一直由代码替我完成——说到底,我只是用Python爬虫和量化思路,给自己造了一个"私人股票分析师"。

这篇文章会把整套思路完整拆开:从数据采集、存储清洗,到指标计算、策略回测,再到上线后遇到的各种坑。不需要你懂多高深的金融理论,只要会一点Python基础、知道pandas和requests大概怎么用,就能跟上节奏。哪怕你完全是为了学爬虫来的,也能从这套真实项目里看到数据采集、反爬应对、SQLAlchemy入库的完整链路。

1. 为什么你需要一个"私人分析师":从需求到系统架构

先明确一个概念:量化交易不是"全自动炒股",更不是"躺着赚钱"。它本质上是一个决策辅助系统——把人为的情绪、犹豫、直觉干扰降到最低,用规则驱动判断。而私人分析师的核心任务,就是把"看一眼K线、翻一翻新闻再做决定"这种模糊过程,变成"指标A触发、条件B满足、仓位C执行"的确定性流程。

1.1 这个系统到底解决了什么问题

先算一笔时间账。手动看盘,一个人要盯几十只票,每只票要看日K、成交量、各种技术指标的变化,开盘前至少要花半小时到一小时,而且容易漏掉关键变动。如果让代码来做,数据采集加指标计算加信号生成,整个过程不超过一分钟。这不是效率提升的问题,是维度区别。

第二个问题是纪律性。人最大的敌人是手痒。涨了想追,跌了想割,这是人性,不是能力问题。但如果你把交易规则写死在代码里,比如"MA5上穿MA20且RSI小于70时列入观察名单",那么当这个条件不满足时,系统就是什么都不推。它不会受情绪影响,这就是规则化决策的价值。

第三点是可复盘性。代码跑过的每一天都会留下数据记录,今天为什么给出这个信号、当时的市场环境长什么样,都能回溯。手动看盘很难做到这种程度的回溯。

1.2 整体架构:分层设计才能走远

我先用一张表把系统分四层,每一层只做自己的事,层与层之间不互相掺和:

层级职责核心技术典型产出
数据层从公开接口获取行情与基础信息requests、API调用股票列表、日K数据
存储层清洗、去重、增量持久化SQLAlchemy + SQLite/MySQL本地数据库表
计算层技术指标、信号合成pandas、numpy指标字段、触发信号
输出层结果展示与主动推送定时任务、终端打印当日关注列表、告警信息

这四层互相独立,好处是出问题能快速定位。比如指标算错了,不需要去怀疑爬虫出了问题,只需要检查计算层的输入输出即可。这也是我给很多新手朋友的建议:第一版不要想着做一个大而全的"系统",先把数据链路打通,后面加什么都简单。

1.3 技术选型怎么定:requests还是scrapy,SQLite还是MySQL

选型这块我直接说结论,然后解释为什么。

  • 爬虫框架:用requests配合Session,不用scrapy。理由是行情数据接口是标准的HTTP JSON接口,结构固定、频率不高、单机完全够用。scrapy是重型框架,适合大规模分布式采集海量网页,这里用不上。
  • 数据存储:用SQLAlchemy ORM,底层先SQLite,数据量大了再切MySQL。SQLAlchemy的ORM能帮你平滑切换数据库,这是它最大的价值:一开始在SQLite上调试逻辑,以后真要换库,只改连接字符串。
  • 计算与回测:pandas + numpy完全够用,不需要引入专门的回测框架。回测框架虽然功能全,但你要先花大量时间看它的API文档,而且规则定义会被框架的套路限制。第一版自己写一个20行的回测循环,逻辑完全可控。

这里有个非常重要的心法:第一版永远用你最熟悉、能最快跑通的技术栈,不要为了"专业"而引入一个没接触过的重量级框架。很多人的量化项目死在第一步,不是技术不行,是系统设计过度复杂了。

2. 数据采集层:别研究K线了,先学会喂饱模型

整个系统的地基是数据。数据不对,后面算的指标全是垃圾。所以采集层的核心目标就一句话:稳定、完整、增量地拿到你需要的行情数据。

2.1 数据源怎么选:东方财富、腾讯、新浪接口对比

国内股票公开数据接口现在主要就几家,我实际用下来各有特点:

数据源接口形式优点缺点
新浪财经hq.sinajs.cn/list=sh600000简单、返回快字段少、偶尔403
腾讯财经qt.gtimg.cn/q=sh600000稳定、字段较多需要处理编码
东方财富push2his.eastmoney.com/api/qt/stock/kline/get数据最全、有复权参数略复杂

我自己主用东财,原因是它的K线接口直接支持前复权、后复权、不复权三种模式,这个后面讲复权时你会明白有多重要。另外它单次最多能返回几百根K线,一天一条数据的话,一次性拿五年完全没问题。

如果你只是拿某一只票的数据试水,可以先从腾讯接口开始,返回的是简单的字符串,用split就能解析,对新手非常友好。但要做到批量、稳定的系统,东财是更好的选择。

2.2 批量获取股票代码:先用列表接口,再逐只拉K线

这步是实现"分析师"的关键:你的自选列表只有十只票,就把这十只票的K线一次性拉全,存在数据库里,以后每天增量更新。但如果你想从4000多只票里筛选,就先把股票列表拉下来。

东财的股票列表接口长这样:

import requests def get_stock_list(): url = "http://push2.eastmoney.com/api/qt/clist/get" params = { "pn": 1, # 页码 "pz": 100, # 每页数量 "po": 1, "np": 1, "fltt": 2, "invt": 2, "fid": "f3", # 按涨跌幅排序 "fs": "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23", # 沪深主板+创业板等 "fields": "f1,f2,f3,f4,f5,f6,f7,f12,f13,f14", # f12为代码,f14为名称 } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "http://quote.eastmoney.com/", } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json()["data"]["diff"] return [{"code": item["f12"], "name": item["f14"]} for item in data]

注意这里一定要带上Referer头,否则容易被拦截。东财有分页机制,pz最大可以拉500个,你可以把pn循环几次拿完全量。这个接口我测试过,比手动网页翻盘要快几个量级。

2.3 日K线采集:核心逻辑与参数细节

拿到代码列表后,就可以用K线接口批量拉数据。K线接口用的是secid参数,这个参数有讲究:上交所股票是"1.600000"这样的格式,深交所股票是"0.000001"这样的格式。前面那个占位数字代表交易所,别搞反。

def get_kline(code, market=1, days=1000): secid = f"{market}.{code}" url = "https://push2his.eastmoney.com/api/qt/stock/kline/get" params = { "secid": secid, "fields1": "f1,f2,f3,f4,f5,f6", "fields2": "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61", "klt": 101, # 101代表日K线 "fqt": 1, # 1前复权 2后复权 0不复权 "beg": "20200101", "end": "20500101", } headers = {**BASE_HEADERS} resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json()["data"]["klines"] rows = [] for item in data: parts = item.split(",") # 日期,开盘,收盘,最高,最低,成交量,成交额,振幅,涨跌幅,涨跌额,换手率 rows.append({ "date": parts[0], "open": float(parts[1]), "close": float(parts[2]), "high": float(parts[3]), "low": float(parts[4]), "volume": float(parts[5]), "amount": float(parts[6]), "amplitude": float(parts[7]), "pct_change": float(parts[8]), "change": float(parts[9]), "turnover": float(parts[10]), }) return rows

拿到的是CSV格式字符串,split处理最简单。我把每一个字段都解析成结构化字典,方便后面直接塞进SQLAlchemy。采集的过程有没有做过重试机制很重要,网络抖动在长跑时一定会发生。我的做法是加一个简单的retry装饰器,失败三次才放弃。

2.4 请求频率与反爬的平衡:别把你的IP玩没了

行情接口的限频不像社交媒体爬虫那么严,但也不代表你可以无限请求。我早期犯过一个错:用for循环一次性拉四百只票,每只之间不设间隔,结果跑了五分钟就被封了IP,一个小时才恢复。

后来我总结出一套保守但稳定的节奏:

  • 单只票K线请求之间,间隔0.2秒到0.5秒。
  • 每拉50只票,强制sleep两秒。
  • 如果遇到429或者403状态码,退避重试,第一次等待两秒,第二次四秒,第三次八秒,呈指数退避。

这套节奏虽然慢一点,但一天增量更新最多几百只票,完全能接受。长期稳定不被封,比一时拉得快重要多了。另外,requests的Session对象要复用,它默认会使用连接池,能省下重复握手的时间。

3. 干净的数据才是策略的基石:清洗与持久化

很多做量化的朋友会忽略掉一个关键问题:你从接口拿到的原始数据,和水晶球一样干净吗?我告诉你,一点都不干净。停牌、缺失、编码、复权跳变,这些坑每一个都能让你的策略算出离谱的结果。

3.1 拿到手的数据长什么样:复权、停牌、缺失值

先说复权。东财的fqt参数有三个值:0是不复权,1是前复权,2是后复权。什么叫复权?当股票发生分红送股时,股价会突然跳空,比如除权日股价从20块直接变成15块,这不是市场砸盘,是股本变大了,每股对应的净资产变少。如果不做复权处理,你的均线、MACD这些指标会在除权日出现一个假拐点,然后策略会给出莫名其妙的信号。

前复权的意思是以当前价为基准,把过去的价格做一个比例调整,让价格曲线连续。这就是为什么我基本只选fqt=1的原因——做技术分析,你看到的历史价格应该是连续、无跳空的,这样指标计算结果才可信。

再说停牌。股票停牌当天,K线接口不会返回那天的记录,这会导致时间序列不够连续。如果你后续用shift(1)做昨日收益率计算,停牌前后的计算结果会出错。解决办法在下游用reindex补全日期,缺失值填充为上一交易日的收盘价。

3.2 SQLAlchemy建表与增量更新:为什么别用CSV

我知道很多入门教程喜欢让学生把数据存成CSV,确实简单,但放到长期维护的项目里,CSV有致命问题:并发写入不安全、增量更新要么全量覆盖要么手动处理,一张几万行的表格,加载速度也会拖慢计算。所以我从一开始就用SQLAlchemy建表。

股票日线表的ORM模型可以这么写:

from sqlalchemy import create_engine, Column, Integer, String, Float, Date, UniqueConstraint from sqlalchemy.orm import declarative_base Base = declarative_base() class StockDaily(Base): __tablename__ = "stock_daily" id = Column(Integer, primary_key=True, autoincrement=True) code = Column(String(10), nullable=False, index=True) date = Column(Date, nullable=False) open = Column(Float) close = Column(Float) high = Column(Float) low = Column(Float) volume = Column(Float) amount = Column(Float) pct_change = Column(Float) __table_args__ = (UniqueConstraint("code", "date", name="uniq_code_date"),) def __repr__(self): return f"<StockDaily {self.code} {self.date}>"

关键在这两个点:一是code和date的组合唯一约束,这是防止重复数据的关键;二是用ORM定义,这样后续插入数据时直接用session.add,不用手写SQL。

3.3 增量更新策略:不重复拉历史,每天只拉缺失数据

全量采集只需要做一次,以后每天运行都走"增量更新"逻辑。核心思路是查一下库里每个股票代码的最大日期,然后从那一天开始重新拉取。

def get_latest_date(session, code): latest_date = session.query(func.max(StockDaily.date)).filter(StockDaily.code == code).scalar() return latest_date or datetime(2020, 1, 1).date() def update_stock(session, code, market): latest = get_latest_date(session, code) beg = (latest - timedelta(days=10)).strftime("%Y%m%d") # 多拉几天防止漏数据 rows = get_kline(code, market, beg=beg) for row in rows: row_date = datetime.strptime(row["date"], "%Y-%m-%d").date() if row_date > latest: obj = StockDaily(code=code, date=row_date, **row) session.add(obj) session.commit()

注意我故意把开始日期往前推了10天,这是为了弥补"节假日无数据"和"接口偶尔延迟导致某天数据未返回"的空档。插入时因为有唯一约束,重复的日期会直接报错或跳过,你可以在批量插入时用session.execute(insert().prefix_with("OR IGNORE"))来做幂等插入。

3.4 数据校验:入库前多看一眼,策略才能少踩坑

数据入库前,我建议做这几项基本校验,能帮你拦住脏数据:

  • 检查关键字段是否为None或NaN,尤其是收盘价。
  • 检查同一天同一个代码是否重复出现。
  • 检查收盘价、最高价、最低价的逻辑关系——最低价应该小于等于收盘价,最高价应该大于等于收盘价,如果不满足,这行数据一定有问题。

这个校验逻辑不要做得多复杂,简单跑一遍就行,关键是让它在每次采集后自动执行,不要依赖人工抽查。我吃过一次亏:某天返回的数据里收盘价字段解析出错变成了字符串,入库后所有指标全计算出NaN,回测结果直接不能看。从那以后,入库前的类型检查就成了固定步骤。

4. 量化指标计算:把数字变成交易信号

数据到位后,就进入整个系统最好玩的部分——把"裸价格"变成"有语义的信号"。这一层做的事情是:读取股票的日K数据,用pandas Vectorized操作计算各类技术指标,最后按照你定义的规则生成交易信号。

4.1 MA、RSI、MACD:三个最基础的指标,够用且好用

技术指标成千上万,但我个人觉得,对于第一版"私人分析师",三个指标足够了:MA均线系统判断趋势方向,RSI判断超买超卖,MACD确认趋势加速或减速。这仨覆盖了趋势跟踪和震荡识别两个维度。

计算方式用pandas实现非常简单:

def calculate_indicators(df): # MA均线:5日、10日、20日、60日 for n in [5, 10, 20, 60]: df[f"ma{n}"] = df["close"].rolling(window=n).mean() # RSI:14日相对强弱指标 delta = df["close"].diff() gain = delta.clip(lower=0) loss = -delta.clip(upper=0) avg_gain = gain.rolling(window=14).mean() avg_loss = loss.rolling(window=14).mean() rs = avg_gain / avg_loss df["rsi14"] = 100 - (100 / (1 + rs)) # MACD:12日EMA - 26日EMA,信号线取9日EMA exp12 = df["close"].ewm(span=12, adjust=False).mean() exp26 = df["close"].ewm(span=26, adjust=False).mean() df["macd"] = exp12 - exp26 df["macd_signal"] = df["macd"].ewm(span=9, adjust=False).mean() df["macd_hist"] = df["macd"] - df["macd_signal"] return df

有几点需要特别说明:

  • adjust=False是必须的,pandas默认的EWM计算方式是调整式,和股票软件里常用的指标公式不一致。要用非调整式才算得出你在同花顺里看到的MACD数值。
  • RSI计算中如果avg_loss为0,说明这段时间只有涨没有跌,RS为无穷大,RSI约等于100。处理时可以把结果clip在0到100之间。
  • rolling窗口的数值和不同软件有差异,比如RSI有9日和14日两种口径,我一般用14日,这是市场主流的默认值。

4.2 向量化计算:为什么不要用for循环

用Python写量化计算,一条铁律就是:能用向量化运算就用向量化。什么是向量化?简单说,就是对整个Series做操作,而不是一行一行地循环。

比如MA5,用df["close"].rolling(5).mean()是一次性算完整列;如果用for循环遍历每一行,3000行数据要循环3000次,这个差别在单只股票上感觉不到,但如果你要算500只股票,循环版本耗时就是向量化的几十倍。

pandas里常用的向量化操作包括:

  • diff():计算前后差值。
  • rolling().mean()/.std()/.max()/.min():滑动窗口统计。
  • ewm().mean():指数加权移动平均。
  • shift(n):整列向下平移n行,这是算收益率、做信号延迟判断的关键。

4.3 信号合成:别只信单一指标,条件叠加才有意义

单一指标的问题是误报太高。比如RSI低于30你买进,在下跌趋势中可能越买越跌。所以我会把多个条件叠加,形成"复合信号",准确率会明显高一些。

举个例子,我的"趋势确认买入"规则:

def generate_signal(df): df = df.copy() # 信号列,1表示买入关注,-1表示卖出预警,0表示无操作 df["signal"] = 0 # 条件:MA5上穿MA20,且MACD柱从负转正 ma5_above_ma20 = (df["ma5"] > df["ma20"]) & (df["ma5"].shift(1) <= df["ma20"].shift(1)) macd_turn_positive = (df["macd_hist"] > 0) & (df["macd_hist"].shift(1) <= 0) df.loc[ma5_above_ma20 & macd_turn_positive, "signal"] = 1 # 条件:MA5下穿MA20,或MACD柱从正转负 ma5_below_ma20 = (df["ma5"] < df["ma20"]) & (df["ma5"].shift(1) >= df["ma20"].shift(1)) macd_turn_negative = (df["macd_hist"] < 0) & (df["macd_hist"].shift(1) >= 0) df.loc[ma5_below_ma20 | macd_turn_negative, "signal"] = -1 return df

这里有个关键的细节:判断"上穿"不能只看当前值大于均线,还要看前一刻是否小于均线,这就是shift(1)的用途。你要是漏掉shift,会把"已经在均线之上"的每一天都判定为金叉点,信号会爆掉。

这套规则写好后,每天跑一次增量数据更新,然后对所有自选股执行这个函数,就能得到一份完整的信号清单。

5. 策略回测:先问代码,再问自己

策略写完之后,最激动也最容易走偏的环节就是回测。回测的目的不是说"这个策略历史收益高所以未来也高",而是验证"在设定的规则下,信号出现后是否能获得合理的统计结果"。很多人回测时掉进了"高收益陷阱",原因多半是没处理好前视偏差和手续费。

5.1 自己写一个20行的简化回测框架

我不想一上来就推荐复杂的第三方回测库,其实核心逻辑很简单:遍历每一天,判断当前是否有信号;有买入信号且没有持仓时,第二天开盘买入;有卖出信号且持仓时,第二天开盘卖出;记录每一笔交易。

def run_backtest(df, init_cash=100000): df = df.reset_index(drop=True) cash = init_cash position = 0 # 持股数量 buy_price = 0 trades = [] for i in range(1, len(df)): # 只能用前一天收盘后产生的信号,当天开盘执行,防止未来函数 signal = df.loc[i - 1, "signal"] date = df.loc[i, "date"] open_price = df.loc[i, "open"] if signal == 1 and position == 0: position = int(cash * 0.95 / open_price / 100) * 100 # 留5%备用 cash -= position * open_price * 1.0003 buy_price = open_price trades.append({"date": date, "type": "buy", "price": open_price}) elif signal == -1 and position > 0: cash += position * open_price * 0.9997 # 扣除手续费 trades.append({"date": date, "type": "sell", "price": open_price, "return": open_price / buy_price - 1}) position = 0 # 如果最后还持仓,按最后收盘价平仓 if position > 0: cash += position * df.iloc[-1]["close"] * 0.9997 final_value = cash return final_value, trades

这个框架有几个重要约定:

  • 信号延迟执行:第i-1天产生的信号,第i天开盘才执行。绝不能第i天收盘算出的信号,第i天收盘去买卖,那是典型的未来函数。
  • 手续费:买入万三、卖出万三加印花税千一,我用0.0003和0.0007近似。很多人回测看着翻倍,实际跑起来收益少一截,就是没算手续费。
  • 仓位控制:每次只投95%的现金,防止因一手股票价格过高导致无法整百买入。

5.2 收益率、夏普比率、最大回撤:三个最关键的体检指标

回测完不能只看"最后赚了多少钱",要看风险调整后的收益。至少算这三个指标:

累计收益率

total_return = final_value / init_cash - 1

年化收益率:把累计收益率换算成年化。假设回测跨度是N个交易日,年化就是(1 + total_return) ** (252 / N) - 1,252是全年大概的交易日数量。

夏普比率:它衡量的是"每承受一单位风险能获得多少超额回报",超出一倍以上的水平就说明是策略在起作用,而不是运气。

# 需要构建每日收益率序列 df["daily_return"] = df["close"].pct_change() sharpe = (df["daily_return"].mean() - risk_free_rate / 252) / df["daily_return"].std() * (252 ** 0.5)

无风险利率现在可以取一年期国债收益率,大约两个点上下。注意要除以252折算成日收益率,因为你的收益序列是按天计算的。

最大回撤:从任意高点回落到低点的最大幅度,直观表达"你最惨的时候亏损了多少"。

cum_return = (1 + df["daily_return"]).cumprod() rolling_max = cum_return.cummax() drawdown = (cum_return - rolling_max) / rolling_max max_drawdown = drawdown.min()

最大回撤40%的策略,就算总收益高,你可能也很难拿住,因为中间过程太煎熬了。所以回测一定要连回撤一起看。

5.3 前视偏差与过拟合:回测都会骗你

回测最容易骗人的地方有两个。

第一个是前视偏差,也叫未来函数。比如你用了当天的收盘价做判断又用当天收盘价买入,这在现实中无法实现,因为收盘价只能在收盘后才知道。我在上面的框架里特意把信号和交易分成两天,就是为了杜绝这个问题。量化界有一句话叫"如果你在回测里看到了未来,那最后的结果就是被未来打脸"。

第二个是过拟合。你去优化参数时发现MA5/MA20配RSI14收益最高,换成RSI13收益就差很多。这说明策略只是在历史数据上背下了答案,而不是找到了规律。常规做法是划分样本内样本外区域:用样本内数据调参,用样本外数据验证。样本外表现还能说的过去,才说明策略有泛化能力。

6. 实测上线后踩过的坑:完整的排查过程记录

我必须老实说,上面那些代码在我本地跑得通,不等于你的环境跑得通。整个系统从"能跑"到"稳定跑",中间隔了不知道多少个坑。我把印象最深的几个记录下来,每一个都是我自己踩进去又爬出来的。

6.1 明明浏览器能打开接口,requests却返回403

排查过程如下:先用浏览器打开接口URL,发现完全正常。再用requests直接请求,返回403。这个现象说明服务器检测到了爬虫特征,而不是接口本身有问题。

我试着加了一个普通UA头,还是403。加上Referer头,恢复正常。所以关键不是UA的问题,是Referer——东财的行情接口要求请求来源必须是它的页面域名,这是最常见的反爬校验之一。

最终我直接用了一个带常用UA和Referer的常量头,一大半403问题就消失了。如果还不行,再看看Cookie。有一次我发现自己session里带了一个过期的Cookie,反而被拦截。处理办法是主动清空Cookie,只保留必要的请求头。

6.2 数据量一多,SQLite写入越来越慢:连接池与WAL模式

前期数据量小的时候,SQLite写入毫无压力。跑到第三个月,数据库里积累了十几万条日K记录,每次增量更新需要插入几百条数据,然后整体速度突然变慢,最夸张的一次更新跑了将近一分钟。

排查思路是这样的:先看是不是单条插入太慢——确实,我当时用session.add循环插入,400条数据要开400次隐式事务。优化方向是改成批量插入用session.bulk_save_objects或者session.execute(insert(), list_of_dicts),这个改动带来接近10倍的提速。

然后我发现数据库的锁等待问题依然存在。SQLite默认是rollback journal模式,读写互相阻塞。解决方式是用一行PRAGMA打开WAL模式:

from sqlalchemy import event from sqlalchemy.engine import Engine @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() cursor.execute("PRAGMA journal_mode=WAL") cursor.execute("PRAGMA synchronous=NORMAL") cursor.close()

WAL模式下,读和写可以并行,日常增量更新几乎不再遇到锁等待。如果你打算长期用SQLite,这个设置一定要加上。

6.3 除权除息日数据跳变:前复权数据的隐性延迟

又一个经典坑。某只股票当天除权,股价从30块跳到25块。我的策略在除权前持有,回测显示除权那天亏损16%,但实际账户一分钱没亏,因为送的红股也折算到持仓里了。

问题的根因是我在历史回测时用了前复权数据,但增量更新时接口默认也返回前复权,这就会导致一个问题:前复权数据每个交易日可能因为最新的分红事件而整体更新,历史价格会随之变化,你的历史信号是基于旧的价格算出来的,但信号已经落库了,不会重新计算。

这是量化系统的经典难题:用前复权历史数据做回测,但每次有新分红事件时,历史K线会整体重写,导致历史信号和最新数据对不上。

我的处理方式是:数据库里同时存"前复权"和"不复权"两个价格序列。指标计算、回测验证用前复权,但账户盈亏的判断用不复权——因为真实账户的买入卖出价格都是不复权价。这样虽然存储量翻倍,但能避免很多逻辑混乱。

6.4 定时任务编排:别用sleep循环,用系统的cron

系统稳定之后,我把它部署在服务器上,希望每天早上开盘前自动更新一次数据、跑一次指标、推送结果。一开始我用Python的while True + sleep实现定时,后来发现这玩意有两个问题:日志中断后无法恢复,进程死了没人拉起来;睡眠循环还占用系统资源。

正确做法是用crontab。每天的定时任务配置如下:

# 周一至周五早上8点30分运行 30 8 * * 1-5 cd /home/quant && /usr/bin/python3 run_daily.py >> logs/daily.log 2>&1

关键点在于日志要重定向到文件,不然cron跑完了什么输出都看不到,出问题都不知道原因。同时建议把run_daily.py里每次跑完打印一行"OK"和统计信息,这样你可以用tail -f logs/daily.log快速确认系统健康。

7. 关于这套系统的边界思考与经验总结

写到这里,核心代码和运行思路基本都交代完了。最后分享一点我在真实使用中形成的体会。

这套系统最大的价值,不在于给你一个"稳赚不赔"的信号,而在于它强迫你把投资逻辑想清楚。你可以不看任何行情软件,每天只打开数据库里的信号清单,看看今天有没有符合条件的标的。如果没有,就安心做自己的事,不用手动翻几百个页面。如果出现了信号,再去结合基本面、新闻做二次判断——代码负责排序,人才负责决策。

从爬虫的角度看,这套东西也是一个小型数据工程的完整打通:requests采集、SQLAlchemy持久化、pandas处理、定时任务调度,每一步都是真实项目里会遇到的技能,不是教程里那种循环打印"hello world"。

如果打算上生产,我有几条实际建议:第一,数据备份比代码重要,数据库每天自动备份一次,出问题一天内可恢复。第二,接口文档会变,东财的参数结构偶尔会调整,所以解析层要单独封装,接口变了只改一个文件。第三,不要把全部资金押在任何单一策略上,系统只能辅助决策,后果要自己承担。

最后分享一个小技巧:把每次触发信号的截图或者数据快照存下来,比如"2025-03-14 600005信号1:MA金叉 + MACD翻红"。几个月后再回头对比,你会对自己的策略有更直观的认识——这套系统不仅能帮你分析股票,还能帮你分析自己的决策习惯。这也许才是私人分析师最容易被低估的价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询