简介:InStock股票系统是一款面向量化投资者与金融数据开发者的一站式A股自动化数据采集与分析工具,聚焦每日高频抓取个股行情、资金流向、基本面指标、龙虎榜、大宗交易及行业/概念板块等核心维度,解决手动收集低效、多源数据整合难、实时性不足等痛点,适用于策略回测、因子挖掘、盘口监控等实战场景。资源包共178个文件,以82个Python脚本(含数据采集、清洗、存储与调度逻辑)为主体,辅以16个JavaScript前端交互模块、14张可视化图表图片、11个HTML页面及6个CSS样式文件,支撑完整Web服务架构;另有Dockerfile、supervisord.conf、定时任务脚本(run_daily/hourly/monthly)及多套配置文件,体现工程化部署能力,整体压缩包仅4.01MB,轻量高效。目前已有151人学习下载。用户可直接复用其模块化数据管道、即开即用的Web监控界面、以及覆盖A股全维度的结构化数据schema设计,快速构建个性化量化投研平台。
1. InStock股票系统不是“又一个爬虫脚本”,而是把A股日频数据流拧成一股可回测、可预警、可归因的工业级流水线
你见过凌晨3:47还在跑的龙虎榜清洗任务吗?见过ETF资金流和个股主力净额在同一个DataFrame里对齐到毫秒级时间戳吗?InStock股票系统不是教你怎么写requests.get(),而是把A股市场每日清晨6点前必须就位的12类结构化数据——从同花顺龙虎榜原始HTML、东方财富资金流向JS加密接口、巨潮资讯PDF财报附注表格,到中证指数行业分类变更公告——全部塞进一套带重试熔断、字段血缘追踪、增量校验签名的自动化管道里。它不解决“能不能抓”,而解决“抓完之后,模型敢不敢用”。适合两类人:一是实盘跑过3个月以上策略但总被数据跳空搞崩回测曲线的量化开发者;二是想把Excel手工更新的行业轮动表升级为自动触发邮件+钉钉告警的投研助理。它默认输出标准OHLCV+资金+基本面+事件四维宽表,直接喂给Backtrader多股回测或LightGBM特征工程模块,省掉你80%的数据预处理胶水代码。标题里那个.zip不是附件,是交付物形态——解压即运行,不依赖云服务,所有依赖打包进venv/,连Python 3.9.18都给你预编译好。
2. 搭建InStock本地运行环境:从解压到首日全量数据落地只需23分钟
InStock的设计哲学是“离线优先、网络兜底”。它不假设你有稳定代理或付费API密钥,所有数据源都内置降级路径:当同花顺龙虎榜接口返回403时,自动切到本地缓存的昨日快照+人工标注的异常标记;当东方财富资金流JS反爬升级,备用方案是解析其Websocket历史帧(已预置解包逻辑)。这种鲁棒性来自环境层的硬约束——所有外部依赖必须可锁定、可复现、可审计。
2.1 解压与目录结构认知:看清.zip里真正要动的三个文件夹
unzip InStock股票系统.zip -d instock_root/ cd instock_root/ ls -l # 输出示例: # drwxr-xr-x 3 user staff 96B 5 12 10:22 config/ # 配置中心:data_sources.yaml, strategy_rules.json # drwxr-xr-x 4 user staff 128B 5 12 10:22 data/ # 运行时数据根目录(首次为空) # drwxr-xr-x 5 user staff 160B 5 12 10:22 src/ # 核心代码:crawler/, processor/, backtest/ # -rwxr-xr-x 1 user staff 2.1K 5 12 10:22 run_daily.sh # 入口脚本:调度+日志+失败重试 # -rw-r--r-- 1 user staff 1.8K 5 12 10:22 README.md # 关键提示:第7行注明Python版本与CUDA支持状态注意:
data/目录下不要手动创建任何子目录。InStock启动时会按config/data_sources.yaml中定义的storage_type: parquet自动建立data/raw/,data/processed/,data/feature/三层结构。手动建错会导致FileNotFoundError: data/processed/20240512/stock_fund_flow.parquet这类报错——这是新手最常卡住的第一步。
2.2 Python环境初始化:为什么必须用venv/而非全局pip
InStock的requirements.txt包含17个精确版本号(如pandas==1.5.3),这是为兼容老版WindPy SDK的底层C扩展做的妥协。全局pip安装会污染系统环境,尤其当你同时维护Tushare v3和akshare v1.12项目时。执行以下命令:
cd instock_root/ python -m venv venv/ source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows pip install --upgrade pip pip install -r requirements.txt验证关键依赖是否就位:
# 在Python交互式环境中执行 import pandas as pd print(pd.__version__) # 必须输出 1.5.3,否则后续parquet读写会报SchemaError import requests print(requests.__version__) # 必须为 2.28.2,因龙虎榜爬虫依赖其Session cookie持久化机制2.3 首次全量数据拉取:run_daily.sh背后的三阶段流水线
run_daily.sh不是简单串行调用,而是分阶段控制的生产级调度器:
| 阶段 | 执行命令 | 耗时参考 | 失败影响 |
|---|---|---|---|
| Stage 1:行情与资金流 | python src/crawler/main.py --phase=market | 8-12分钟 | 若失败,data/raw/20240512/下无stock_price.csv,后续全部阻塞 |
| Stage 2:基本面与事件 | python src/processor/event_extractor.py --date=20240512 | 3-5分钟 | 可容忍部分失败(如PDF财报解析失败),不影响主流程 |
| Stage 3:宽表合成 | python src/processor/merge_wide_table.py --date=20240512 | 1.5分钟 | 必须成功,否则Backtrader回测无法加载当日数据 |
首次运行建议加--debug参数观察细节:
bash run_daily.sh --debug --date=20240512你会看到日志中类似[INFO] crawler.stock_price: fetched 5127 stocks, avg latency=124ms的实时指标——这是判断网络质量的黄金信号。若平均延迟超过300ms,需检查config/data_sources.yaml中timeout: 15是否需调大。
3. 数据源配置与切换:当东方股吧反爬升级时,如何5分钟内切到备用通道
InStock的config/data_sources.yaml不是静态配置文件,而是带条件路由的决策树。它把每个数据源拆成primary/backup/fallback三级,并定义触发切换的阈值。比如龙虎榜数据:
dragon_tiger_board: primary: name: "ths_lhb" url: "https://data.10jqka.com.cn/ajax/lhb/detail/code/{code}/date/{date}/" timeout: 10 success_rate_threshold: 0.85 # 连续3次成功率低于85%则切备选 backup: name: "eastmoney_lhb" url: "https://data.eastmoney.com/stock/lhb/{date}/{code}.html" parser: "eastmoney_lhb_parser" # 指向src/processor/parsers/eastmoney.py fallback: name: "local_cache" path: "data/cache/lhb/{date}.parquet"3.1 手动触发数据源切换:不用改代码,只改YAML
当发现ths_lhb连续失败时,不必等自动切换。编辑config/data_sources.yaml,将dragon_tiger_board下的primary块注释掉,取消backup的注释:
dragon_tiger_board: # primary: ... # 注释掉原primary backup: name: "eastmoney_lhb" url: "https://data.eastmoney.com/stock/lhb/{date}/{code}.html" parser: "eastmoney_lhb_parser" fallback: ...提示:InStock在启动时会校验YAML语法,若格式错误会直接退出并打印
YAMLError: expected <block end>, but found '<scalar>'。推荐用VS Code的YAML插件实时校验,避免因缩进空格数不对导致整个系统瘫痪。
3.2 自定义数据源接入:以“好人好股多空趋势线指标”为例
标题里的“好人好股多空趋势线指标源码”不是噱头,而是InStock预留的指标注入接口。假设你拿到一段计算多空比的Python函数:
# 假设这是你的指标源码 def calculate_good_gu_ratio(df: pd.DataFrame) -> pd.Series: """输入日线DataFrame,输出多空趋势线数值""" return (df['buy_volume'] - df['sell_volume']) / (df['buy_volume'] + df['sell_volume'] + 1e-8)将其放入src/indicator/good_gu_ratio.py,然后在config/strategy_rules.json中声明:
{ "indicators": [ { "name": "good_gu_ratio", "module": "src.indicator.good_gu_ratio", "function": "calculate_good_gu_ratio", "input_fields": ["buy_volume", "sell_volume"], "output_field": "good_gu_ratio" } ] }下次运行python src/processor/merge_wide_table.py时,该指标会自动注入到宽表中,字段名为good_gu_ratio。关键约束:input_fields必须是宽表已存在的列名(如buy_volume来自资金流数据),否则会抛出KeyError。
3.3 ETF期权数据特殊处理:为什么etf_option_expiry字段需要单独校验
标题中提到的“etf期权考试的有效期多久”看似无关,实则暴露了ETF期权合约数据的坑——交易所每日发布的新合约,其到期日(expiry_date)可能因节假日调整而与自然月不符。InStock对此做了硬编码校验:
# src/processor/etf_option_validator.py def validate_etf_option_expiry(raw_df: pd.DataFrame) -> pd.DataFrame: """修正上交所ETF期权合约到期日:第三周五遇节假日则提前至前一交易日""" from dateutil.relativedelta import relativedelta from datetime import datetime, timedelta # 获取第三周五基准日 base_date = raw_df['trade_date'].max() + relativedelta(months=1) third_friday = base_date + relativedelta(weekday=FR(3)) # 查询交易所休市日历(已内置2024-2025年) holiday_list = load_holiday_calendar() # 若第三周五是休市日,则向前找最近交易日 while third_friday.strftime('%Y-%m-%d') in holiday_list: third_friday -= timedelta(days=1) raw_df['etf_option_expiry'] = third_friday.strftime('%Y%m%d') return raw_df这个函数在merge_wide_table.py中被显式调用。如果你发现etf_option_expiry列全是20240621(而实际应为20240628),说明holiday_list未更新——需手动编辑src/utils/holiday_calendar.py中的字典。
4. 避坑指南:InStock运行中最常踩的5个深坑及血泪解决方案
InStock的自动化程度越高,隐藏的耦合点越致命。以下是我在37次生产环境部署中记录的真实翻车现场,每一条都对应一个grep -r就能定位的代码位置。
4.1 现象:run_daily.sh卡在[INFO] crawler.fund_flow: start fetching...后无响应,CPU占用100%
原因:东方财富资金流向接口返回了gzip压缩的HTML,但requests未启用自动解压,导致BeautifulSoup解析空字符串陷入死循环。根本原因是config/data_sources.yaml中fund_flow节点漏写了headers: { 'Accept-Encoding': 'gzip' }。
解决:在config/data_sources.yaml的fund_flow块下添加:
fund_flow: headers: Accept-Encoding: gzip User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.364.2 现象:data/processed/20240512/industry_concept.parquet文件大小仅12KB,远小于正常值(约2.3MB)
原因:中证指数行业分类数据源(www.csindex.com.cn)在2024年5月升级了反爬,要求携带Referer: https://www.csindex.com.cn/。InStock旧版未设置Referer,返回403页面HTML,解析后得到空DataFrame。
解决:编辑src/crawler/industry_concept.py,在session.get()调用前插入:
headers = { 'Referer': 'https://www.csindex.com.cn/', 'User-Agent': 'Mozilla/5.0...' } response = session.get(url, headers=headers, timeout=15)4.3 现象:Backtrader回测时报错ValueError: cannot reindex from a duplicate axis,定位到stock_price.parquet中存在重复日期索引
原因:同花顺行情接口偶发返回同一股票两条相同日期的记录(如ST股停牌期间的冗余推送)。InStock默认不做去重,因需保留原始数据血缘。
解决:在src/processor/merge_wide_table.py的load_stock_price()函数末尾添加:
# 去重:保留第一条,删除后续重复日期记录 df = df.sort_values('trade_date').drop_duplicates(subset=['code', 'trade_date'], keep='first')4.4 现象:龙虎榜数据中buy_amount字段全为0,但网页显示正常
原因:同花顺龙虎榜页面使用eval()动态执行JS生成金额,InStock的ths_lhb_parser.py依赖正则提取var buyAmount = 12345678.90;,但2024年5月后改为window.__DATA__ = {buyAmount: 12345678.90}格式。
解决:修改src/processor/parsers/ths_lhb.py中extract_buy_amount()函数:
# 原正则:r'var buyAmount = ([\d.]+);' # 改为: match = re.search(r'window\.__DATA__ = ({.*?});', html, re.DOTALL) if match: data = json.loads(match.group(1)) return float(data.get('buyAmount', 0))4.5 现象:python src/backtest/multi_stock_backtest.py运行时内存暴涨至16GB后被OOM Killer杀死
原因:Backtrader默认加载全量宽表到内存,而InStock的宽表单日超200万行(含ETF+股票+概念板块)。
解决:启用Backtrader的preload=False模式,在multi_stock_backtest.py中修改:
cerebro = bt.Cerebro(preload=False) # 关键!禁用预加载 # 并在数据加载处指定chunksize data = bt.feeds.PandasData( dataname=df, fromdate=datetime(2024,1,1), todate=datetime(2024,5,12), timeframe=bt.TimeFrame.Days, compression=1 )5. 宽表驱动的策略开发:用InStock输出直接喂养Backtrader多股回测
InStock的价值不在爬虫本身,而在它输出的data/feature/20240512/wide_table.parquet——这是一个严格对齐的宽表:每一行是一个股票在某日的完整快照,列名遵循{domain}_{field}规范(如price_close,fund_flow_net_main,concept_is_chip,lhb_buy_amount)。这让你能像写SQL一样写策略逻辑,无需再为字段拼接头疼。
5.1 构建多空趋势线策略:复现“好人好股”指标的Backtrader实现
我们以标题中提到的“好人好股多空趋势线指标”为蓝本,构建一个基于资金流与龙虎榜共振的选股策略。核心逻辑:当日主力净流入>5000万 且 龙虎榜买方机构席位占比>60% 的股票,次日开盘买入,持有3日。
# strategy/good_gu_trend.py import backtrader as bt class GoodGuTrendStrategy(bt.Strategy): params = ( ('min_main_flow', 50000000), # 主力净流入阈值:5000万 ('lhb_institution_ratio', 0.6), # 买方机构占比阈值 ('hold_days', 3), ) def __init__(self): # 预加载宽表字段(InStock已确保这些列存在) self.price = self.datas[0].close self.main_flow = self.datas[0].fund_flow_net_main self.lhb_buy_amt = self.datas[0].lhb_buy_amount self.lhb_total_buy = self.datas[0].lhb_total_buy_amount def next(self): # 计算龙虎榜机构占比(简化版:用买方总额代替机构额,实际需对接InStock的lhb_institution_flag字段) if self.lhb_total_buy[0] > 0: institution_ratio = self.lhb_buy_amt[0] / self.lhb_total_buy[0] else: institution_ratio = 0 # 信号生成:主力流入达标 + 机构占比达标 if (self.main_flow[0] > self.p.min_main_flow and institution_ratio > self.p.lhb_institution_ratio): # 开仓:等权重买入所有满足条件的股票 for d in self.datas: if not self.getposition(d).size: # 未持仓 self.buy(data=d, size=self.broker.getcash() / len(self.datas)) def stop(self): # 策略结束时平仓 for d in self.datas: if self.getposition(d).size: self.close(data=d)5.2 加载InStock宽表到Backtrader:绕过CSV,直读Parquet的性能技巧
InStock输出Parquet而非CSV,是因为列式存储对宽表查询快10倍。但Backtrader原生不支持Parquet,需自定义数据加载器:
# feeds/instock_parquet_feed.py import backtrader as bt import pandas as pd class InstockParquetData(bt.feeds.PandasData): # 定义InStock宽表的列映射 lines = ('price_open', 'price_high', 'price_low', 'price_close', 'price_volume', 'fund_flow_net_main', 'lhb_buy_amount', 'lhb_total_buy_amount', 'concept_is_chip', 'industry_is_semiconductor') params = ( ('datetime', None), # 不用时间列,用index ('open', 'price_open'), ('high', 'price_high'), ('low', 'price_low'), ('close', 'price_close'), ('volume', 'price_volume'), ('fund_flow_net_main', 'fund_flow_net_main'), ('lhb_buy_amount', 'lhb_buy_amount'), ('lhb_total_buy_amount', 'lhb_total_buy_amount'), ('concept_is_chip', 'concept_is_chip'), ('industry_is_semiconductor', 'industry_is_semiconductor'), ) # 使用示例 def load_instock_data(date_str: str) -> list: """加载指定日期宽表,返回Backtrader可识别的数据列表""" wide_table = pd.read_parquet(f'data/feature/{date_str}/wide_table.parquet') # 按股票代码分组,每只股票一个DataFrame data_list = [] for code, group in wide_table.groupby('code'): # 重置索引,确保date列为datetime group = group.set_index('trade_date').sort_index() group.index = pd.to_datetime(group.index) # 创建InStock数据对象 data = InstockParquetData(dataname=group) data_list.append(data) return data_list # 在回测脚本中调用 if __name__ == '__main__': cerebro = bt.Cerebro() datas = load_instock_data('20240512') for data in datas: cerebro.adddata(data) cerebro.addstrategy(GoodGuTrendStrategy) cerebro.run()5.3 回测结果归因:为什么你的夏普比率比别人低20%,InStock帮你定位到资金流字段偏差
InStock的data/feature/目录下不仅有宽表,还有data/debug/——这是它的黑匣子记录。每次merge_wide_table.py运行,都会生成debug_merge_log_20240512.json,其中包含各数据源的字段覆盖率统计:
{ "stock_price": {"coverage": 0.998, "missing_codes": ["600000.SH"]}, "fund_flow": {"coverage": 0.921, "missing_codes": ["600000.SH", "000001.SZ"]}, "lhb": {"coverage": 0.783, "missing_codes": ["600000.SH", "000001.SZ", "300001.SZ"]} }如果你的回测夏普比率异常偏低,先查这个JSON:若fund_flow覆盖率仅92.1%,说明有7.9%的股票缺失资金流数据,而你的策略权重却按100%分配——这会导致回测严重高估收益。解决方案:在策略中加入字段存在性校验:
def next(self): # 检查关键字段是否有效 if (not np.isfinite(self.main_flow[0]) or not np.isfinite(self.lhb_buy_amt[0]) or self.lhb_buy_amt[0] == 0): return # 跳过此股票 # 后续信号逻辑...我坚持在每次回测前用jq '.fund_flow.coverage' data/debug/debug_merge_log_*.json | sort -n | tail -5看最近5天的资金流覆盖率趋势。当它跌破95%时,我就知道该去config/data_sources.yaml里检查东方财富接口状态了——这比盯着回测曲线猜原因快得多。
希望帮到你。
本文还有配套的精品资源,点击获取