油价暴涨,92号迈入9元时代:Python量化实战,能源板块数据获取与技术指标分析
Python · akshare · pandas · 布林带 · RSI · 能源板块
2026年3月,国际油价一路狂奔,布伦特原油期货价格悄然站上了每桶90美元的关键整数关口。这不是演习,也不是预测,这是现实。加油站显示屏上的数字正在一次次刷新,车主群里弥漫着一种说不清道不明的焦虑感。有人开始算账:一个月通勤油费多了两三百块,这钱从哪儿来?与此同时,知乎热榜上"92号汽油进入9元时代"的话题一路冲到了前五十,热度突破八十万。
油价的波动从来不只是开车族的私事。顺着这条链往上看,炼化企业、化工产业链、航空航运、物流运输——几乎每一个工业门类都在承受成本端的压力。但换一个视角,对于量化交易者来说,宏观变量的剧烈波动往往伴随着板块内部的结构性机会。能源价格上涨,产业链上下游的利润分配会发生迁移,某些细分领域的公司会因为成本转嫁能力更强而更值得关注。
今天这篇文章,我想用Python把这件事做扎实一点。不是泛泛而谈油价走势,也不是推荐哪个板块,那些事留给经济学家和分析师去做。我要做的事情是:亲手搭建一套从数据获取到技术指标计算到初步筛选的完整流程,把能源板块的主要个股历史数据拉到本地,用布林带和RSI这两个经典指标跑一遍,真实感受一下当前市场的技术位置。
工具链很简单:akshare负责数据,pandas负责清洗和处理,指标计算手工实现——不需要依赖任何商业库。一套下来代码量不大,但能说明的问题不少。
一、先把工具装好:akshare是什么
做量化分析,第一步永远是数据。没有数据,所有的技术指标、策略回测都只是纸上谈兵。akshare是一款专门为Python量化分析设计的开源金融数据包,由国内开发者维护,涵盖了股票、期货、基金、宏观数据、数字货币等多个品类的实时和历史数据接口。
它的最大优点是免费、数据源稳定、接口文档清晰。对于不想折腾Wind、Choice这些商业终端的个人投资者来说,akshare几乎是目前最优的替代方案。当然,它不是完美的——部分接口有时会因为上游数据源调整而短暂失效,但这属于可以接受的技术噪声,遇到问题更新到最新版本通常能解决。
二、安装与环境准备
整个分析流程只需要三个库:akshare用于数据获取,pandas用于数据处理,numpy用于数值计算。指标计算我们手工实现,不依赖talib,避免了Windows上安装talib的兼容性烦恼。
# 安装akshare(推荐最新版本)
pip install akshare -U
# 安装pandas和numpy(通常已预装,没有的话加上)
pip install pandas numpy
装好之后验证一下是否能正常调用:
>>> import akshare as ak
>>> import pandas as pd
>>> import numpy as np
>>> print("环境就绪,版本检查:")
>>> print(f"akshare: {ak.__version__}")
>>> print(f"pandas: {pd.__version__}")
>>> print(f"numpy: {np.__version__}")
环境就绪,版本检查:
akshare: 1.14.18
pandas: 2.2.3
numpy: 1.26.4
说明:不同时间安装的版本号可能有所差异,只要能正常import就说明没问题。akshare更新比较频繁,遇到某个接口报错可以先尝试pip install akshare -U更新到最新版本。
三、获取能源板块成分股数据
能源板块在A股有明确的行业分类。根据中信行业分类标准,能源板块主要包括石油开采、石油化工、煤炭开采、燃气等细分领域。akshare提供了按行业板块获取成分股的功能,我们先用这个接口拿到能源板块的股票列表。
import akshare as ak
import pandas as pd
import numpy as np
# 获取同花顺行业板块列表
energy_df = ak.stock_board_industry_name_em()
print("成功获取行业板块列表,部分行业如下:")
print(energy_df.head(10))
上面的代码会返回一个包含各行业板块名称和代码的DataFrame。找到"石油行业"、"煤炭行业"对应的板块代码之后,就可以获取成分股列表了:
# 获取"石油行业"板块的成分股
oil_stocks = ak.stock_board_industry_cons_em(symbol="石油行业")
print(f"石油行业成分股数量:{len(oil_stocks)}")
print(oil_stocks[['代码','名称','最新价','涨跌幅']].head(10))
# 获取"煤炭行业"板块的成分股
coal_stocks = ak.stock_board_industry_cons_em(symbol="煤炭行业")
print(f"\n煤炭行业成分股数量:{len(coal_stocks)}")
print(coal_stocks[['代码','名称','最新价','涨跌幅']].head(10))
这里拿到的成分股列表包含了股票代码、名称、当前价格、涨跌幅等基础信息。但对于技术分析来说,这些远远不够——我们需要的是至少半年到一年以上的历史K线数据,才能计算布林带和RSI这些趋势类指标。
用akshare获取个股历史K线数据非常简单,接口叫stock_zh_a_hist,参数包含股票代码、开始日期、结束日期和复权方式。为了避免复权数据干扰价格真实性,这里使用前复权(qfq)。
# 获取个股历史K线数据(以中国石化600028为例)
def get_stock_history(stock_code, start_date="20240101", end_date="20260325"):
"""
获取个股历史K线数据
stock_code: 股票代码,如"600028"(中国石化)
返回:包含日期、开盘、收盘、最高、最低、成交量等字段的DataFrame
"""
df = ak.stock_zh_a_hist(
symbol=stock_code,
period="daily",
start_date=start_date,
end_date=end_date,
adjust="qfq" # 前复权
)
# 重命名列(方便后续处理)
df.columns = ['日期','开盘','收盘','最高','最低','成交量','成交额',
'振幅','涨跌幅','涨跌额','换手率']
df['日期'] = pd.to_datetime(df['日期'])
# 按时间正序排列(akshare默认倒序,必须处理)
df = df.sort_values('日期').reset_index(drop=True)
return df
# 示例:获取中国石化近一年日K数据
df = get_stock_history("600028")
print(f"数据范围:{df['日期'].min().date()} 至 {df['日期'].max().date()}")
print(f"总交易日数:{len(df)}")
print(df.tail(5))
成功获取数据之后,你会看到一个类似这样的输出:
数据范围:2024-01-02 至 2026-03-24
总交易日数:545
日期 开盘 收盘 最高 最低 成交量
540 2026-03-18 6.82 6.91 6.95 6.78 84236521
541 2026-03-19 6.88 6.84 6.92 6.80 71325418
542 2026-03-20 6.89 6.95 7.01 6.83 89541257
543 2026-03-21 6.86 6.91 6.88 6.84 65478521
544 2026-03-24 6.93 7.02 7.08 6.89 102547893
说明:akshare的stock_zh_a_hist接口返回的数据是按时间倒序排列的(最新日期在前),在计算技术指标之前必须用df.sort_values('日期')按时间正序排列,否则指标计算会出错。前面封装get_stock_history函数时已经内置了这个处理逻辑。
四、计算布林带与RSI:两种经典技术指标
布林带(Bollinger Bands)和RSI(Relative Strength Index,相对强弱指标)是技术分析中应用最广泛的两个指标。前者衡量价格的相对波动区间,后者衡量近期价格的涨跌力量。对于当前油价波动背景下的能源股分析,这两个指标各有侧重:布林带帮助我们判断股价当前是否处于历史波动的极端位置,RSI则反映短期趋势的强弱变化。
4.1 布林带(Bollinger Bands)
布林带由三条线构成:中轨是N日移动平均线,上轨是中轨加K倍标准差,下轨是中轨减K倍标准差。参数上,N通常取20日(一个月的交易日),K通常取2。布林带的核心逻辑是:价格围绕均值波动,当触及上轨或下轨时,往往意味着短期超买或超卖。
def calculate_bollinger_bands(df, window=20, num_std=2):
"""
计算布林带
window: 移动平均周期,默认20日
num_std: 标准差倍数,默认2倍
"""
df = df.copy()
# 中轨:N日简单移动平均
df['BB_MID'] = df['收盘'].rolling(window=window).mean()
# 标准差
df['BB_STD'] = df['收盘'].rolling(window=window).std()
# 上轨:中轨 + K * 标准差
df['BB_UPPER'] = df['BB_MID'] + num_std * df['BB_STD']
# 下轨:中轨 - K * 标准差
df['BB_LOWER'] = df['BB_MID'] - num_std * df['BB_STD']
# 布林带宽度(衡量波动率)
df['BB_WIDTH'] = (df['BB_UPPER'] - df['BB_LOWER']) / df['BB_MID']
# %B指标:价格在布林带中的相对位置
df['BB_PCTB'] = (df['收盘'] - df['BB_LOWER']) / (df['BB_UPPER'] - df['BB_LOWER'])
return df
# 应用到数据
df = calculate_bollinger_bands(df, window=20, num_std=2)
print(df[['日期','收盘','BB_MID','BB_UPPER','BB_LOWER','BB_PCTB']].tail(10))
4.2 RSI(相对强弱指标)
RSI通过计算一定周期内收盘价上涨和下跌的平均值之比,来判断当前是多头更强还是空头更强。RSI的取值范围是0到100。传统上,RSI超过70被认为是超买区域,低于30被认为是超卖区域。但这个标准并非绝对——在趋势明显的行情中,RSI可以在超买区域停留很长时间,并不必然意味着下跌。
def calculate_rsi(df, period=14):
"""
计算RSI(相对强弱指标)
period: 计算周期,默认14日
"""
df = df.copy()
# 计算每日涨跌
delta = df['收盘'].diff()
# 分离上涨和下跌
gain = delta.where(delta > 0, 0.0)
loss = (-delta).where(delta < 0, 0.0)
# 计算平均涨跌幅(使用指数移动平均)
avg_gain = gain.ewm(alpha=1/period, min_periods=period).mean()
avg_loss = loss.ewm(alpha=1/period, min_periods=period).mean()
# 相对强度
rs = avg_gain / avg_loss
# RSI
df['RSI'] = 100 - (100 / (1 + rs))
return df
# 应用RSI
df = calculate_rsi(df, period=14)
print(df[['日期','收盘','RSI']].tail(15))
运行结果大致如下:
日期 收盘 RSI
530 2026-03-04 6.75 48.23
531 2026-03-05 6.78 51.07
532 2026-03-06 6.84 58.14
533 2026-03-07 6.89 62.39
534 2026-03-10 6.91 63.85
535 2026-03-11 6.88 58.76
536 2026-03-12 6.93 61.42
537 2026-03-13 6.82 52.18
538 2026-03-14 6.87 55.63
539 2026-03-17 6.84 53.21
540 2026-03-18 6.91 56.87
541 2026-03-19 6.84 51.44
542 2026-03-20 6.95 60.23
543 2026-03-21 6.91 57.65
544 2026-03-24 7.02 63.42
说明:RSI的计算方式有两种:简单移动平均(SMA)和指数移动平均(EMA)。上面的代码用的是EMA版本,也是大多数行情软件默认的方式。如果你想用SMA,只需把ewm替换成rolling(period).mean()即可。
五、把整套流程自动化:批量处理能源板块
单只股票分析没有太大意义,我们需要批量处理整个能源板块的所有成分股,然后从技术面角度做一次初步扫描。下面把上面的数据获取和技术指标计算封装成一个完整的扫描函数:
import time
def scan_energy_sector():
"""
扫描能源板块主要个股的技术指标
返回:包含布林带和RSI指标的汇总DataFrame
"""
results = []
# 获取能源板块成分股(石油+煤炭)
for sector in ["石油行业", "煤炭行业"]:
try:
stocks = ak.stock_board_industry_cons_em(symbol=sector)
stock_codes = stocks['代码'].tolist()
print(f"正在扫描{sector},共{len(stock_codes)}只股票...")
for code in stock_codes:
try:
# 获取近一年日K数据
df = ak.stock_zh_a_hist(
symbol=code,
period="daily",
start_date="20250101",
end_date="20260325",
adjust="qfq"
)
df.columns = ['日期','开盘','收盘','最高','最低','成交量',
'成交额','振幅','涨跌幅','涨跌额','换手率']
df['日期'] = pd.to_datetime(df['日期'])
df = df.sort_values('日期').reset_index(drop=True)
# 计算布林带
df = calculate_bollinger_bands(df, window=20)
# 计算RSI
df = calculate_rsi(df, period=14)
# 取最新一行数据
latest = df.iloc[-1]
stock_name = stocks[stocks['代码']==code]['名称'].values[0]
results.append({
'代码': code,
'名称': stock_name,
'板块': sector,
'最新收盘': round(latest['收盘'], 2),
'BB_MID': round(latest['BB_MID'], 3),
'BB_UPPER': round(latest['BB_UPPER'], 3),
'BB_LOWER': round(latest['BB_LOWER'], 3),
'BB_WIDTH': round(latest['BB_WIDTH'], 4),
'BB_PCTB': round(latest['BB_PCTB'], 2),
'RSI_14': round(latest['RSI'], 2),
'近一年涨跌': round((latest['收盘']/df.iloc[0]['收盘']-1)*100, 2)
})
time.sleep(0.3) # 降低请求频率,避免被限速
except Exception as e:
print(f" {code} 处理失败:{str(e)[:50]}")
continue
except Exception as e:
print(f"获取{sector}成分股失败:{e}")
return pd.DataFrame(results)
# 执行扫描
summary = scan_energy_sector()
print("\n=== 扫描结果(按RSI从高到低排序)===")
print(summary[['名称','板块','最新收盘','BB_PCTB','RSI_14','近一年涨跌']].sort_values('RSI_14', ascending=False))
# 保存结果到CSV
summary.to_csv("energy_sector_scan_20260325.csv", index=False, encoding="utf-8-sig")
print("结果已保存到 energy_sector_scan_20260325.csv")
扫描完成之后,你会得到一个包含每个能源股BB_PCTB和RSI数值的汇总表,并自动保存为CSV文件,方便在Excel里进一步筛选和可视化。扫描结果大概长这样:
=== 扫描结果(按RSI从高到低排序)===
名称 板块 最新收盘 BB_PCTB RSI_14 近一年涨跌
0 某石化 石油行业 7.02 0.87 63.42 8.35%
1 某石油 石油行业 5.41 0.72 58.16 5.21%
2 某煤炭 煤炭行业 8.65 0.54 52.38 3.42%
3 某燃气 燃气行业 4.28 0.31 45.67 -2.15%
...
结果已保存到 energy_sector_scan_20260325.csv
六、如何解读这些指标:找值得关注的方向
数据跑出来了,接下来是怎么用的问题。布林带和RSI只是工具,关键在于怎么用工具看问题,而不是被工具牵着走。
先说布林带。BB_PCTB这个指标非常直观:当BB_PCTB接近1.0甚至超过1.0时,说明股价运行在布林带上轨附近,属于技术面上的相对高位,短期回调风险加大。当BB_PCTB接近0甚至为负时,说明股价贴近甚至跌破布林带下轨,属于技术面上的相对低位,可能存在反弹机会。
再看RSI。RSI在50以上代表多头占优,50以下代表空头占优,这是最基础的判断。但更精确的用法是结合具体数值区间:
RSI低于30的区域通常被视为超卖区域,意味着短期卖压可能已经释放得比较充分。当然,在强趋势中RSI可以长时间维持在超卖区域不动,所以超卖不等于立刻反弹,但至少说明向下空间可能有限。RSI超过70则进入超买区域,尤其是当RSI在70以上创出新高时,需要警惕短期回调风险。
把两个指标结合起来看,胜率会更高。比如:一只股票BB_PCTB处于0.1以下的低位(贴近布林带下轨),同时RSI也低于30(处于超卖区域),这种组合往往意味着技术面上存在双重支撑。虽然不保证立刻反弹,但至少下行风险相对有限。
反过来,如果一只股票RSI已经超过75,BB_PCTB也接近1.0,那就需要非常谨慎了。这种双重高位信号说明短期动能可能已经过度消耗,调整的概率会明显增加。
结合当前的油价背景来看,逻辑会更清晰一些。国际油价上涨,能源企业的直接受益逻辑是:原油价格上涨,开采类企业利润增厚,股价获得基本面支撑。但这个传导链条有时候快,有时候慢,而且市场会提前反映。所以,当技术指标显示某只能源股RSI和BB_PCTB都处于相对低位,但基本面逻辑依然成立的情况下,这类标的值得重点关注。反之,如果技术面已经充分甚至过度反映了油价上涨的利好,那就要多一分谨慎。
七、完整脚本分享:拿来即用
把上面的所有代码整合起来,就是一个完整的能源板块技术面扫描脚本。完整版本如下,可以直接复制到一个Python文件里运行,不需要安装任何商业库:
"""
能源板块技术指标扫描脚本
功能:批量获取能源板块个股数据,计算布林带和RSI
作者:真龙现身
日期:2026-03-25
"""
import akshare as ak
import pandas as pd
import numpy as np
import time
# ==================== 指标计算函数 ====================
def calculate_bollinger_bands(df, window=20, num_std=2):
df = df.copy()
df['BB_MID'] = df['收盘'].rolling(window=window).mean()
df['BB_STD'] = df['收盘'].rolling(window=window).std()
df['BB_UPPER'] = df['BB_MID'] + num_std * df['BB_STD']
df['BB_LOWER'] = df['BB_MID'] - num_std * df['BB_STD']
df['BB_WIDTH'] = (df['BB_UPPER'] - df['BB_LOWER']) / df['BB_MID']
df['BB_PCTB'] = (df['收盘'] - df['BB_LOWER']) / (df['BB_UPPER'] - df['BB_LOWER'])
return df
def calculate_rsi(df, period=14):