☰
股票期权分析实战:从数据清洗到建议引擎的完整决策流程
2026/9/26 5:19:50 网站建设 项目流程

简介:这是一份面向股票与期权交易者、金融数据分析爱好者的Python项目资料,围绕标的筛选、期权挑选、预测分析与决策建议展开。站点基于Excel原型搭建,主文件夹包含CSS样式,数据层集成历史波动率、Wyckoff方法的多空判断,并规划有推荐引擎、博弈论辅助、计算器及即将加入的机器学习模块;第一个选项卡提供摘要分析与三十日/一年期价格估计,第二个选项卡负责推荐逻辑,整体适合希望将数据分析、可视化和预测模型应用于投资场景的Python学习者参考。从预览信息看,站点保留了Excel原型的设计思路,并基于开源数据整理股票价格与期权相关指标,适合作为量化投研与机器学习辅助决策的入门模板。资源以zip压缩包形式发布,大小约325.82MB,文件总数与具体类型明细暂无数据。已有162人浏览学习,项目模块划分清晰、可二次开发,适合作为构建期权分析工具、补充量化投研思路的实践样例。

1. 股票期权分析项目:不是又一个预测模型,而是把决策拆成可计算步骤

这份「股票期权分析」项目,我拆完第一感觉是:它没有把宝押在「预测准不准」上,而是把股票挑选、期权挑选、预测性分析、建议引擎和盈亏计算器打包成了一整套决策流程。你输入一个股票池,它先算出每只股票的上涨概率和风险指标,再基于历史波动率筛出值得关注的期权合约,最后用博弈论思路告诉你「该不该动手、动手买哪个」,盈亏计算器则把你最坏和最好的结果都摊在眼前。适合想用 Python 做量化分析和期权交易的从业者,尤其是对 pandas、机器学习建模有一定基础,但还没把「预测结果」转化成「下单动作」的人。这个项目让我意识到,期权分析的关键不是概率多准确,而是从数据到动作的每一环都别断掉。

2. 数据准备与特征工程:先拉干净数据,再谈预测模型

2.1 从 yfinance 拉行情:复权价与时间窗的选择

做股票期权分析,第一步不是写模型,而是把行情数据搞干净。这个项目用的是 yfinance 作为数据源,虽然它现在没有官方 API,但胜在免费、无需 token,个人研究和复现足够用。我一般会先用它拉日线数据,再把列名统一成小写,避免后面写特征时大小写翻车。

import yfinance as yf import pandas as pd def fetch_stock_data(ticker, start="2020-01-01", end="2024-12-31"): df = yf.download(ticker, start=start, end=end, auto_adjust=True) df.rename(columns=lambda x: x.lower(), inplace=True) df["return_1d"] = df["close"].pct_change() return df df = fetch_stock_data("AAPL") print(df.tail())

这段代码里有两个关键参数。auto_adjust=True表示拿到的 price 已经按拆股和股息做了复权,这样计算收益率不会出现因为除权造成的假跳空;return_1d是当天相对前一天的收益率,后面算波动率、构造标签都要用它。注意rename这一步不能省,yfinance 返回的列名是大写的Close、Open,pandas 列名区分大小写,统一小写后写df["close"]才不会报 KeyError。

时间窗的选择也有讲究。我一般把训练窗口设为 4 年左右,覆盖至少一个完整的涨跌周期;预测窗口则根据你要预测的持有周期来定,项目里默认用 5 日后的涨跌作为标签,对应短线期权仓位。如果你的策略是周频或月频,就把shift(-5)改成shift(-20),同时特征窗口也要同步调整,否则特征周期和标签周期不匹配,模型学出来的东西会怪。

2.2 技术指标与标签构造:别把未来函数写进去

特征工程这一步,项目里封装了一个add_features函数,把均线、RSI、波动率、目标标签一次性算完。很多新手在这里最容易踩坑:用当天的收盘价去预测当天,或者用了未来信息后没剔除,回测结果好看到假。

def add_features(df, window=14): close = df["close"] df["ma_5"] = close.rolling(5).mean() df["ma_20"] = close.rolling(20).mean() delta = close.diff() gain = delta.clip(lower=0).rolling(window).mean() loss = (-delta.clip(upper=0)).rolling(window).mean() rs = gain / loss df["rsi"] = 100 - (100 / (1 + rs)) df["volatility"] = df["return_1d"].rolling(window).std() * (252 ** 0.5) df["target"] = (close.shift(-5) > close).astype(int) return df.dropna()

这里有三个地方值得细看。第一个是 RSI 的计算:gain用的是下跌时的零值平均,loss用的是上涨时的零值平均,二者相除得到相对强度,再转成 0-100 区间。第二个是volatility的年化处理,把日收益标准差乘以 252 的平方根,换算成年化波动率,期权定价里 IV 和这个量直接可比。第三是target:用close.shift(-5)拿到 5 日后收盘价,再和今天比较,得到 1/0 标签,这样模型学的是「未来 5 天是否上涨」,而不是当天涨跌。dropna()会把指标还没收敛的前 14 行删掉,避免把空值喂给模型。

如果你用这套特征直接跑机器学习,我建议先做个相关性检查。像ma_5和ma_20高度相关是正常的,但决策树类模型不敏感;如果你换成线性模型,就最好先标准化或剔除冗余特征。项目里默认是梯度提升树,所以没有做特征缩放,这是合理的。

2.3 参数表与数据清洗边界

下面这张表是项目里比较核心的几个参数,我复现时照着默认值跑了一遍,基本没出大问题。如果你想迁移到自己的股票池,改的最多的就是 ticker、start/end 和 window。

参数默认值作用我调整时注意的点
tickerAAPL标的代码换成你的股票池,需保证 yfinance 能拉到
start/end2020/2024数据区间至少覆盖一个涨跌周期
auto_adjustTrue复权设为 False 会导致除权日收益率异常
window14RSI/波动率窗口与标的交易频率强相关,日线用 14
shift 天数5预测持有期改成 20 时,特征窗口也要放大

数据清洗的边界比很多人想的要宽。除了去掉空值,还要检查是否有交易日缺失(比如美股节假日)、是否有价格突变(拆股未复权时会出现)。我一般会在fetch_stock_data后面加一行df = df.loc[df["volume"] > 0],把停牌日剔掉。项目里的 dropna 会处理大部分问题,但如果你发现某只股票的数据量比其他股票少很多,大概率是上市时间短或者退市了,这时候要么补全,要么直接跳过,别硬算。

另外要强调一点:yfinance 拉到的数据是「生存者偏差」的一部分。你现在能拉到的都是还活着的股票,如果你用这个数据集做全市场回测,会高估策略表现。所以项目更适合用于已知股票池的「决策辅助」,而不是用来发现「未来会涨的股票」。这个认知决定了我后面怎么解读预测结果。

3. 预测模型与建议引擎:机器学习给概率,博弈论给动作

3.1 价格方向预测:梯度提升与时间序列交叉验证

特征造好之后,项目里的预测部分用的是GradientBoostingClassifier。为什么选它而不是 LSTM 或者 XGBoost?因为这份资源的定位是「能跑起来、能解释」,梯度提升对表格数据非常友好,不需要做特征缩放,调参下限低,而且可以输出概率,后面做建议引擎要用概率而不是单纯的 0/1 标签。常见做法是先拿决策树模型跑通,再根据效果换成更重的模型。项目里默认参数已经能出一个不错的基准。

from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import classification_report def train_model(df, features): X = df[features] y = df["target"] split_idx = int(len(df) * 0.8) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] model = GradientBoostingClassifier( n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42 ) model.fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test))) return model

这个切分方式是「按时间顺序」的 8/2 切分,不是随机切分,这是对的。时间序列数据如果随机打乱,会让模型偷看未来信息,测试集分数虚高。但我必须提醒你:train_model里的单次时间切分仍然不够稳健,因为实验窗口可能正好是某一段特殊行情。我通常在跑通后直接用后续的滚动回溯替代这次切分,那个结果才敢作为调参依据。

参数上,n_estimators=200控制树的棵数,太小容易欠拟合,太大会过拟合且有显存压力;max_depth=3限制每棵树深度,防止模型记住噪声;learning_rate=0.05是每棵树的贡献步长,调小一点能提升泛化,但需要更多棵树。如果训练集和验证集准确率差距很大,先降learning_rate,再增n_estimators,而不是无脑调深度。项目里还开了random_state=42,保证每次运行可复现。

3.2 基于博弈论的建议:为什么「预测涨」不等于「该买入」

拿到模型输出的上涨概率后,项目里并没有简单粗暴地「概率大于 0.5 就买入」。这正是它标题里「建议」二字的落点。它引入了一个非常轻量的博弈论思路:你作为交易者,面对的不是一张静态概率表,而是一个由对手盘组成的市场。你买入后赚到的钱,本质上来自对手盘的损失,所以需要考虑「如果你赢了能赢多少,输了会输多少」。

项目里实现了一个简化的recommend_action函数,它把预测概率、当前仓位和最大亏损容忍度组合成一个决策分数。虽然不包含严格的纳什均衡推导,但决策逻辑体现了博弈论里的 maxmin 思想——先看最坏情况是否可承受,再考虑期望收益。

def recommend_action(prob_up, position_pct, max_loss_tolerance=0.05): # 假设单次收益波动 ±3%,用概率加权成期望收益 expected_gain = prob_up * 0.03 - (1 - prob_up) * 0.03 if expected_gain > 0.005 and position_pct < max_loss_tolerance: action = "买入" elif expected_gain < -0.005: action = "减仓" else: action = "持有" return {"action": action, "expected_gain": expected_gain, "prob_up": prob_up}

这段代码里的position_pct是该股票在你组合里的仓位占比,max_loss_tolerance是你最多允许它亏掉总资金的比例。当期望收益超过千分之五且仓位没超限才买入;当期望收益为负则减仓,其余情况持有。这个推荐不是让你照单全收,而是把「预测」翻译成「动作」,并且把风险敞口量化出来。如果你自己改,可以把 3% 换成从历史收益分布里算出的真实波动幅度,把 0.005 换成你的盈亏平衡点。

3.3 建议输出样式:置信区间与决策表

项目最终的建议不是一行干巴巴的「买入」,而是一个结构化的结果表。每个 ticker 的输出字段包括模型概率、期望收益、动作、建议的开仓仓位。我复现时把recommend_action套在run_recommendation上,连续跑了好几个月的滚动预测,发现它的价值不在预测多准,而在强制你去想「如果概率只有 0.55,我该不该买」。这在期权场景里尤其重要,因为期权有权利金成本,你的胜率必须高于某个阈值才能覆盖成本。

def run_recommendation(ticker, df, features, max_loss_tolerance=0.05): model = train_model(df, features) # 用最新一个交易日的数据预测 latest_features = df[features].iloc[[-1]] prob_up = model.predict_proba(latest_features)[:, 1][0] rec = recommend_action(prob_up, position_pct=0.02, max_loss_tolerance=max_loss_tolerance) rec["ticker"] = ticker return rec

这个函数把训练和推荐连在一起。注意,在实际使用中应该用历史数据训练,只对最后一天做预测,而不是把训练集和测试集都包含在同一个 df 里再 predict。如果上了生产环境,通常会把训练好的模型用joblib.dump存下来,后续每天拉新数据后只做特征工程和 predict,不再重新训练。下面的表是典型输出字段的样式,数据是我为了演示随便造的,字段格式是项目里真实存在的:

字段示例值含义
tickerAAPL股票代码
prob_up0.585日上涨概率
expected_gain0.006期望收益(百分比)
action买入建议动作
suggested_position0.04建议仓位占组合比例

这个表格对期权交易的指导意义在于:当你对某只股票形成「买入」建议后,下一步才轮到期权挑选。很多新手反过来,先看到某个期权便宜就买,完全不管标的本身的上涨概率。项目里建议引擎放在预测之后、期权筛选之前,就是要你把顺序捋正。

4. 期权挑选与盈亏计算器:把行权价、到期日和权利金算明白

4.1 期权链抓取与筛选逻辑:delta 和隐含波动率怎么用

预测建议给出了标的方向,但期权交易还要选行权价和到期日。项目里用 yfinance 直接拉期权链,然后按 delta 和隐含波动率过滤。这个选择很务实,因为个人拿实时期权链的成本很高,yfinance 的期权链延迟半小时左右,但用来做策略参考足够。

import yfinance as yf def get_options_chain(ticker, expiry): tk = yf.Ticker(ticker) chain = tk.option_chain(expiry) calls = chain.calls calls["ticker"] = ticker calls["expiry"] = expiry return calls calls = get_options_chain("AAPL", "2025-06-20") print(calls[["strike", "lastPrice", "delta", "impliedVolatility"]].head())

option_chain(expiry)返回一个对象,里面包含 calls 和 puts 两个 DataFrame。需要注意的是expiry必须是 yfinance 认可的标准到期日格式,一般是每月的第三个星期五,直接用字符串可能找不到。我一般在代码里先调用tk.options拿到所有可用的到期日列表,再选离目标持有期最近的日期,否则经常报错。

def nearest_expiry(ticker, target_days=30): tk = yf.Ticker(ticker) expiries = tk.options # 返回所有可用到期日字符串 for exp in expiries: days = (pd.to_datetime(exp) - pd.Timestamp.today()).days if 20 <= days <= target_days + 20: return exp return expiries[0]

这里的目标是找一个 20 到 60 天之间到期的合约,太便宜不选,太贵不选。到期日太近,theta 衰减很快;太远,权利金里时间价值占比太高。筛选逻辑上,我一般只看 delta 绝对值在 0.3 到 0.5 之间的期权。delta 接近 0.5 表示平值附近,对股价波动最敏感,适合做方向性交易;delta 太小(虚值)便宜但胜率低,权利金归零风险大。隐含波动率我会和历史波动率比较,如果 IV 比 HV 高出 20% 以上,说明期权被高估,不应该做买方,可以考虑卖方策略。

4.2 盈亏计算器:单腿和组合策略的到期收益

项目里的计算器部分,不是一个复杂的希腊字母计算器,而是把「到期时的盈亏」这条最硬的线画清楚。它允许你输入买入价格、行权价和到期日当天的标的价格,输出这笔期权合约的盈亏。这个功能用来做压力测试特别有用——你可以在买入前把所有可能的价格点都过一遍。

def call_option_pnl(strike, premium, price_at_expiry, contracts=1): multiplier = 100 # 标准美股期权合约对应 100 股 intrinsic = max(price_at_expiry - strike, 0) total_pnl = (intrinsic - premium) * multiplier * contracts return total_pnl def put_option_pnl(strike, premium, price_at_expiry, contracts=1): multiplier = 100 intrinsic = max(strike - price_at_expiry, 0) total_pnl = (intrinsic - premium) * multiplier * contracts return total_pnl

这两个函数就是简单的「末日盈亏」:买入看涨,到期时股价越高赚越多;买入看跌,到期时股价越低赚越多。premium是买入一份权利金的价格(每股),multiplier乘 100 是因为一张期权对应 100 股现货。注意这里没有计算卖出的保证金占用、没有考虑提前行权,也没有把卖出权利金的收益展现出来——如果要做卖出策略,需要把premium前的符号反过来,并在独立账户里加保证金监控。项目里还带了一个plot_options_payoff的数据可视化函数,把到期日价格从 0 到当前价两倍的范围画成盈亏曲线,这个图非常直观。

如果只是单腿还不够,项目里也支持简单的组合策略,比如牛价差:

def bull_call_spread_pnl(strike_short, strike_long, premiums, price_at_expiry, contracts=1): long_pnl = call_option_pnl(strike_long, premiums[0], price_at_expiry, contracts) short_pnl = -call_option_pnl(strike_short, premiums[1], price_at_expiry, contracts) return long_pnl + short_pnl # 示例:买入行权价100,卖出行权价110,两个权利金分别为6和2 pnl = bull_call_spread_pnl(110, 100, [6, 2], price_at_expiry=115)

牛价差的好处是最大亏损有限,最大盈利也封顶,并且权利金净支出比单腿买更低。项目里把这种组合计算器和单腿计算器放在同一个模块里,你只要传入两腿参数就能对比不同策略的盈亏曲线。对期权新手来说,先跑一遍这个计算器,再决定要不要实盘,能避免很多无谓损失。

4.3 期权筛选参数对比表

把计算器和筛选逻辑放一起,我整理了一张参数表,也是我调优时最常改的四个值。

参数推荐范围作用踩坑提示
delta0.3 - 0.5方向敏感度用绝对值判断
impliedVolatility与 HV 对比期权贵贱IV 高时买方成本高
expiry20-60 天时间价值衰减速度太短 theta 损耗剧烈
contracts1 张起仓位控制永远不要因为便宜就加张数

这个表格里最容易被忽视的是expiry。很多人觉得离到期越远越安全,但期权的时间价值衰减在最后 30 天会加快,如果你想做一个 5 日方向的短线预测,选择 20 到 60 天到期的期权,既可以避开最陡的 theta 衰减,又不会因为时间太长导致权利金太高。项目里的默认逻辑会根据预测持有期自动推荐到期日,原理就是尽量让到期日落在你预测窗口的两到三倍之外。

5. 避坑指南:我在复现这套项目时踩过的四个坑

5.1 现象:回测收益率曲线很漂亮,实盘一跑就变脸

我最早跑通项目时,用全部历史数据做了特征和预测,回测收益率曲线看着每年翻倍,兴奋得差点直接开实盘。结果在最近一年的 out-of-sample 数据上跑,准确率直接跌到 51%,和抛硬币没区别。原因出在数据泄露:我在dropna()之前就生成了 target,但dropna()是在所有特征计算完成后执行的,导致模型训练集和测试集之间没有足够的时间间隔,某些滚动指标把未来信息带进了训练。

解决方法是:所有特征和目标都按时间顺序构造,然后用一个独立的 out-of-sample 期间做验证。我这里强制要求训练集和测试集之间至少间隔 20 个交易日,模拟真实交易中「历史数据训练、未来预测」的场景。另外,每次训练前把特征列单独 copy 出来,避免后续操作改到原 DataFrame:

def clean_features(df): feature_cols = ["close", "ma_5", "ma_20", "rsi", "volatility", "target"] return df[feature_cols].copy()

这个copy()极其重要。pandas 的切片返回的是视图,如果你在切片后继续赋值,容易触发 SettingWithCopyWarning,更危险的是悄悄改到原始 df,导致训练集和验证集边界模糊。

5.2 现象:期权到期日格式化后变成 1970-01-01

项目早期版本里我用pd.to_datetime(expiry)直接转换 yfinance 返回的到期日字符串,结果一部分日期变成了 1970-01-01。原因是 yfinance 的期权到期日字符串格式在不同市场不一样,有的带星期几(如 Fri Jun 20 2025),有的纯数字(2025-06-20),to_datetime遇到非标准格式就解析成 epoch 时间。

解决方法是统一用parse_dates先标准化,或者直接取tk.options返回列表里的原始字符串,不做转换。从那以后我所有期权日期都保留字符串,只在显示时转格式:

expiry_str = "2025-06-20" # 保留原始字符串 # 只在需要计算天数时转一次 expiry_dt = pd.to_datetime(expiry_str, format="%Y-%m-%d", errors="coerce")

注意errors="coerce",如果格式无法解析,它会给 NaT 而不是 1970-01-01。这样后续筛选时一眼就能看出脏数据。

5.3 现象:批量拉取几百只股票时内存直接爆炸

这个项目如果跑全股票池,我见过一次内存占用 8GB 的场景。原因不是 yfinance 本身,而是我每只股票都拉了 6 年日线,并存了所有技术指标的中间结果,DataFrame 没有及时清理。解决方法是:训练前只保留模型需要的特征列,使用df[["close", "ma_5", "ma_20", "rsi", "volatility", "target"]]复制一份;同时在fetch_stock_data里加一个downsample=True参数,对超长历史只保留近 5 年。内存问题更常见的来源是rolling().mean()生成了很多临时列,建议每步只保留结果列,中间变量覆盖掉。

我习惯在循环里用del手动释放不再需要的对象,并且用gc.collect()及时回收。如果是按股票池循环,每处理完一只股票就把该股票的 DataFrame 设为None,避免累积占用。这样处理后,同样跑几百只股票,内存峰值能控制到 2GB 以内。

5.4 现象:预测引擎说买入,但我按行权价选期权后亏了

这是最让我意识到「预测和期权挑选不能脱节」的坑。有一次模型对某股票给出 0.62 的上涨概率,我选了 delta 0.45 的浅虚值看涨期权,结果股价只涨了 1%,期权到期归零,因为时间价值和隐含波动率双杀。原因在于我选期权时只看了 delta,忽略了隐含波动率已经很高,权利金里包含了太多「预期波动」成本。

解决方法是把推荐引擎的输出和期权筛选逻辑连起来:当预测概率超过 0.6 且预期涨幅大于 3% 时,才允许购买期权;并且选择 IV 比历史波动率低的合约,否则宁可买入正股。项目后来加了一个should_trade_option函数,把「方向概率」和「波动率成本」两个条件同时检查后,才输出具体的期权行权价。

def should_trade_option(prob_up, expected_move, iv, hv, min_prob=0.6, min_move=0.03): if prob_up < min_prob: return False, "预测概率不足" if expected_move < min_move: return False, "预期涨幅不足" if iv > hv * 1.2: return False, "隐含波动率过高" return True, "满足期权交易条件"

从那以后,我每次跑完预测都会先过一遍这个条件,再去看期权链。这相当于用两个独立的过滤器做交叉验证,减少了单一信号带来的误判。

6. 最后的验证技巧:用滚动回溯而不是一次性切分来评估

无论是预测模型还是期权策略,最怕的就是一次性时间切分给你一个虚假的优越感。我自己吃过亏以后,养成了一个习惯:任何模型的评估都强制走一遍滚动回溯(rolling window backtest),而不是用train_test_split或者单次切分。滚动回溯的思路是:固定训练窗口长度,每走完一个预测周期就向前滚动,把新数据和真实结果纳入下一次训练,这样每一步都是在「当时无法知道未来」的条件下做预测,评估结果才接近真实可复现的上限。

下面这段代码是项目里我后来补上的滚动评估函数,它能输出每个滚动期的概率预测,你也可以直接用它替代train_model里的单次切分:

def rolling_backtest(df, features, train_days=500, step_days=5): probs = [] for start in range(0, len(df) - train_days - step_days, step_days): train = df.iloc[start:start + train_days] test = df.iloc[start + train_days:start + train_days + step_days] model = GradientBoostingClassifier(n_estimators=100, max_depth=3) model.fit(train[features], train["target"]) prob = model.predict_proba(test[features])[:, 1] probs.extend(prob) return probs

这个函数的核心是start不断向前移动,每 5 个交易日重新训练一次模型。注意train_days=500意味着每次只取最近 500 天数据训练,模拟「只使用当时可得的信息」做预测,而不是一次把 1400 天全塞进去。如果你把step_days调成 20,就是每 20 天才重新训练,长持有期策略可以这样。我在跑完滚动回溯后,还会用 matplotlib 画一条「滚动累计准确率」曲线,看模型在不同市场阶段是否稳定,而不是只看一个总准确率。

验证技巧的最后一步,是把这个滚动回溯和期权计算器结合起来。我在每个滚动周期结束时,假设买入当时最贵的合约,用相同本金算一笔盈亏,这样得到的是「策略最终是赚是亏」而不是「预测对不对」。从那以后,我每次评审新模型都会强制走一遍滚动回溯加成本模拟,没走完这两个步骤的模型,我不会相信它的零样本表现。希望这个习惯也能帮到读者,祝你这套项目跑通顺利,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询