简介:本资源是一套面向数据科学初学者与金融领域从业者的Python实战教学包,聚焦金融大数据挖掘与分析全流程能力培养,覆盖数据获取、清洗、建模、可视化到风险评分等核心环节。压缩包共32个文件,含25个Python源码(如Selenium金融爬虫、Tushare数据接口调用、信用评分卡构建、相关性分析及数据库交互脚本)、3个Excel数据文件(含原始行情与评分结果)、2个说明文档及2个README,整体仅118KB,轻量易学。已有2252人下载学习,适合作为高校金融科技课程补充材料或自学进阶实践载体。读者可直接运行案例代码,掌握金融新闻多源抓取、东财/巨潮/新浪等平台数据解析、基于文本与标题的信用评分建模、TS时间序列绘图及SQL数据库联动分析等真实场景技能,所有模块均提供可调试源码与结构化目录,便于分阶段复现与二次开发。
1. 为什么金融数据挖出来全是“噪声”,而别人却能跑出稳定信号?——这不是Python语法问题,是全流程断点没对齐
你下载了那个标着“Python金融大数据挖掘与分析全流程详解案例源码.zip”的压缩包,解压后看到十几个.py文件、一个data/目录和三份PDF文档,兴冲冲双击main.py——报错:ModuleNotFoundError: No module named 'pandas_datareader';换环境重装,又卡在yfinance超时;好不容易拉到数据,发现stock_price.csv里日期是字符串、收盘价带逗号、还有空值混在中间;跑完LSTM预测模型,回测曲线漂亮得像PS出来的,但实盘一挂单就滑点3%,策略净值三天回撤12%……这不是你代码写得差,而是**“全流程”三个字被当成了装饰词**。这个标题指向的,根本不是“用Python写几个金融函数”,而是一条从原始行情接口取数、清洗脏字段、构建多周期特征、处理前视偏差、滚动回测、归因分析到生成可部署信号的工业级链路。它适合两类人:一是刚从量化岗面试失败回来、发现简历上写的“熟悉Python金融分析”在面试官眼里等于“会print(‘hello world’)”的应届生;二是手上有历史策略但总在实盘翻车、怀疑是不是自己漏掉了某个关键校验环节的从业老手。本文不讲Pandas语法糖,只拆解这条链路上每个环节的真实输入输出、必检断点、不可绕过的校验逻辑——所有代码均可本地复现,所有坑都来自我过去三年在券商自营和私募FOF系统里踩过的血泪现场。
2. 从交易所原始接口到结构化DataFrame:为什么90%的人卡在第一步的数据获取层
金融数据挖掘的起点从来不是CSV文件,而是如何让程序像交易员一样“看懂”交易所的实时脉搏。很多教程直接给个pd.read_csv('stock_data.csv')就往下走,这相当于教人开车却不教怎么点火——你永远不知道引擎是否在空转。真正的第一步,是建立一条抗干扰、可审计、带熔断机制的数据获取通道。我们不用pandas_datareader(已弃用且依赖过时API),也不硬套selenium(它本该用于网页交互,而非行情抓取,属于典型工具误配),而是采用yfinance+akshare双源兜底方案:前者覆盖美股、港股、全球指数的免费高频数据,后者专注A股全量财务、宏观、另类数据,且全部基于HTTP长连接+JSON解析,无浏览器开销。
2.1 用yfinance安全拉取美股日线:绕过429限流与时区陷阱
import yfinance as yf import pandas as pd from datetime import datetime, timedelta import time def fetch_us_stock(symbol: str, period: str = "2y") -> pd.DataFrame: """ 安全拉取美股日线数据,内置重试、时区对齐、字段标准化 :param symbol: 股票代码,如 'AAPL', 'TSLA' :param period: yfinance支持的周期,'2y'表示最近2年 :return: 标准化DataFrame,列名统一为 ['open','high','low','close','volume','adj_close'] """ max_retries = 3 for attempt in range(max_retries): try: # 关键:显式指定timezone,避免yfinance返回naive datetime导致后续计算错误 ticker = yf.Ticker(symbol) df = ticker.history(period=period, auto_adjust=True) # 强制转换为UTC时区再转为北京时间(东八区),解决开盘时间错位 if not df.index.tz: df.index = df.index.tz_localize('UTC').tz_convert('Asia/Shanghai') # 重命名列并保留必要字段 df = df.rename(columns={ 'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close', 'Volume': 'volume', 'Adj Close': 'adj_close' })[['open', 'high', 'low', 'close', 'volume', 'adj_close']] # 检查数据完整性:至少要有500条记录(约2年交易日) if len(df) < 500: raise ValueError(f"数据量不足:{symbol} 仅获取 {len(df)} 条记录") return df.dropna() # 删除含NaN的行,但不drop整列 except Exception as e: print(f"[尝试 {attempt+1}/{max_retries}] 获取 {symbol} 失败:{str(e)}") if attempt < max_retries - 1: time.sleep(2 ** attempt) # 指数退避 else: raise RuntimeError(f"连续 {max_retries} 次获取 {symbol} 失败,请检查网络或symbol有效性")提示:这段代码的核心价值不在
yfinance调用本身,而在三处硬性约束:①tz_localize('UTC').tz_convert('Asia/Shanghai')强制时区对齐,否则df.resample('W').last()会把周五收盘算进下一周;②auto_adjust=True启用复权,避免分红送股导致价格跳空;③dropna()是有选择地丢弃,只删掉某一行全为NaN的记录(通常是停牌日),绝不删掉某列(如volume为0是有效信号)。很多新手用df.dropna(axis=1)直接干掉volume列,结果后续波动率计算全错。
2.2 用akshare补全A股财务因子:避开HTML解析黑匣子
selenium出现在热搜里,是因为大量旧教程用它模拟登录东方财富网爬财报——这是典型的“杀鸡用牛刀”。akshare封装了交易所官方接口,返回纯JSON,无需渲染页面:
import akshare as ak def fetch_a_stock_finance(symbol: str) -> pd.DataFrame: """ 获取A股公司最新年报核心财务指标(ROE、PE、资产负债率等) :param symbol: A股代码,如 '600519'(贵州茅台) :return: 包含12个关键财务字段的DataFrame,索引为报告期(如'2023-12-31') """ try: # akshare的finance_indicator接口返回的是字典,需手动转DataFrame data_dict = ak.stock_financial_abstract(symbol=symbol) # 提取关键字段,映射为标准英文列名(便于后续统一建模) key_mapping = { '每股净资产': 'bps', '每股收益': 'eps', '净资产收益率': 'roe', '市盈率': 'pe', '市净率': 'pb', '资产负债率': 'debt_to_asset', '流动比率': 'current_ratio', '速动比率': 'quick_ratio', '营业利润率': 'operating_profit_margin', '净利润率': 'net_profit_margin', '毛利率': 'gross_profit_margin', '存货周转率': 'inventory_turnover' } # 构造DataFrame,确保列顺序固定 df = pd.DataFrame(data_dict).T # 转置使报告期为index df = df.rename(columns=key_mapping) df = df[[col for col in key_mapping.values() if col in df.columns]] # 类型强转:所有数值字段转float,空值填0(财务中0比NaN更合理) for col in df.select_dtypes(include=['object']).columns: df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0) return df.sort_index(ascending=False) # 最新报告期在最前 except Exception as e: print(f"获取 {symbol} 财务数据失败:{e}") return pd.DataFrame(columns=list(key_mapping.values()))注意:
ak.stock_financial_abstract返回的是摘要数据(非完整财报),但它足够支撑多因子选股的初筛。如果你需要现金流量表细节,应切换到ak.stock_zh_a_daily获取日线+ak.stock_fundamental获取季度财报,但必须做报告期对齐——比如2023年报发布于2024年4月,那么2024年1-3月的交易信号必须使用2022年报数据,否则就是前视偏差。这个对齐逻辑,将在第4章的特征工程中展开。
3. 金融数据清洗不是“去空值”,而是重建市场微观结构的时间戳契约
拿到原始数据只是开始,真正的挑战在于:市场数据天生带着“伤疤”——停牌、涨跌停、ST标记、分红除权、指数成分调整……这些不是噪声,是规则。用通用清洗库(如missingno)一键删除缺失值,等于把病人的X光片当废纸扔掉。金融清洗的本质,是用业务逻辑修复时间序列的断裂点。
3.1 处理A股特有的“一字板”与“停牌”:用状态机替代简单插值
A股每日10%涨跌停限制导致大量“一字板”(开盘即封死),其K线表现为high==low==close==open,volume极小。若用df['close'].fillna(method='ffill'),会把涨停日的价格错误延续到次日,扭曲波动率计算。正确做法是标记状态,分场景处理:
def mark_market_status(df: pd.DataFrame) -> pd.DataFrame: """ 为A股日线数据添加市场状态标记,指导后续清洗策略 :param df: 输入DataFrame,必须包含 'open','high','low','close','volume' 列 :return: 新增 'status' 列,取值:'normal','limit_up','limit_down','suspended','st' """ df = df.copy() # 初始化状态列 df['status'] = 'normal' # 1. 停牌判断:成交量=0 且 价格未变(排除集合竞价异常) suspended_mask = (df['volume'] == 0) & (df['high'] == df['low']) & (df['high'] == df['close']) df.loc[suspended_mask, 'status'] = 'suspended' # 2. 涨停判断:收盘价=当日最高价,且较前一日收盘涨10%(考虑ST股5%) # 先计算前一日收盘(用shift,但需处理首行) df['prev_close'] = df['close'].shift(1).fillna(df['close'].iloc[0]) limit_up_mask = ( (df['close'] == df['high']) & (df['close'] >= df['prev_close'] * 1.099) & # 容忍浮点误差 (~suspended_mask) # 排除停牌日 ) df.loc[limit_up_mask, 'status'] = 'limit_up' # 3. 跌停同理 limit_down_mask = ( (df['close'] == df['low']) & (df['close'] <= df['prev_close'] * 0.901) & (~suspended_mask) ) df.loc[limit_down_mask, 'status'] = 'limit_down' # 4. ST标记(需外部ST列表,此处简化为价格异常波动) # 实际项目中应对接akshare.st_stock_list() st_mask = (df['close'] / df['prev_close'] - 1).abs() > 0.055 # 连续两日超5.5% df.loc[st_mask & ~suspended_mask, 'status'] = 'st' return df # 应用状态标记 df_with_status = mark_market_status(df_raw) # 按状态分组清洗:停牌日用前值填充,涨停日保持原值但标记,正常日才做技术指标计算 df_clean = df_with_status.copy() df_clean.loc[df_clean['status'] == 'suspended', ['open','high','low','close']] = \ df_clean['close'].shift(1).fillna(method='bfill') # 停牌日价格用前一日收盘填充逻辑说明:这段代码的关键在于状态驱动清洗。
status列不是为了可视化,而是作为后续所有计算的开关:
- 当计算布林带(Bollinger Bands)时,
rolling(20).std()必须exclude_na=False,但需在status=='suspended'的行上强制设为0;- 当构建动量因子(如20日涨幅)时,
df['close'].pct_change(20)的分母必须是20个交易日前的close,而非日历日——这意味着要跳过所有status!='normal'的日子,用df[df['status']=='normal'].index重新索引;- 这种“状态-动作”映射,才是金融数据清洗的工业级范式,远超
df.dropna()的暴力美学。
3.2 修复复权断点:用除权日清单校准价格连续性
yfinance的auto_adjust=True虽能自动复权,但对A股分红派息的适配存在盲区。例如贵州茅台2023年分红100元/10股,yfinance可能将除权日价格下调9.09元,但实际市场中,除权日开盘参考价=(前日收盘-每股现金分红)/(1+送股比例),而yfinance未区分现金分红与送股。因此,必须用交易所公布的除权除息日清单进行二次校准:
def adjust_dividend_gaps(df: pd.DataFrame, ex_dividend_dates: list) -> pd.DataFrame: """ 基于交易所公布的除权除息日,修正价格序列中的跳空缺口 :param df: 原始日线DataFrame,index为datetime :param ex_dividend_dates: 除权除息日列表,格式如 ['2023-06-28', '2023-12-20'] :return: 修正后的DataFrame,价格序列连续 """ df = df.copy() # 将除权日转为datetime并排序 ex_dates = pd.to_datetime(ex_dividend_dates).sort_values() # 对每个除权日,计算价格调整系数 for ex_date in ex_dates: if ex_date not in df.index: continue # 获取除权日及前一日数据 prev_day = df.index[df.index < ex_date].max() if len(df.index[df.index < ex_date]) > 0 else None if prev_day is None: continue # 计算理论除权价:假设仅现金分红,分红额=前日收盘 - 除权日开盘 # 实际中需从公告提取分红额,此处用差值近似 theoretical_ex_price = df.loc[prev_day, 'close'] - (df.loc[prev_day, 'close'] - df.loc[ex_date, 'open']) # 计算调整系数 = 理论价 / 实际价 actual_ex_price = df.loc[ex_date, 'open'] if actual_ex_price == 0 or theoretical_ex_price == 0: continue ratio = theoretical_ex_price / actual_ex_price # 对除权日及之后所有数据,按ratio缩放价格(volume反向缩放) mask = df.index >= ex_date df.loc[mask, ['open','high','low','close','adj_close']] *= ratio df.loc[mask, 'volume'] /= ratio return df # 使用示例:从akshare获取贵州茅台除权日 ex_dates = ak.stock_zh_a_dividend_detail(symbol="600519", indicator="dividend")['除权除息日'].tolist() df_final = adjust_dividend_gaps(df_with_status, ex_dates)参数说明:
ex_dividend_dates必须来自权威源(如akshare的stock_zh_a_dividend_detail),不能靠价格跳空自动识别——因为主力资金常在除权日前一日打压股价制造“假缺口”。ratio的计算逻辑是逆向工程:用前日收盘与除权日开盘的差值反推分红额,再求出理论除权价,最后得到缩放系数。这个系数作用于adj_close列,确保所有技术指标(MACD、RSI)的计算基线一致。
4. 特征工程不是“加减乘除”,而是用金融直觉编码市场博弈规则
在金融场景中,“特征”不是数学符号,而是市场参与者行为的代理变量。计算一个MA(20)移动平均,本质是在问:“过去20个交易日,买方力量的重心在哪里?”;构造volume_ratio = volume / volume.rolling(10).mean(),是在探测:“今日成交量是否显著异于近期常态,暗示主力进场?”——所有特征必须能被交易员一句话说清业务含义,否则就是数字幻觉。
4.1 构建多周期动量特征:拒绝单一窗口的玄学陷阱
很多教程用df['close'].pct_change(60)作为“长期动量”,但A股市场存在明显的周期嵌套效应:周线动量决定方向,日线动量确认强度,分钟线动量捕捉入场点。单一窗口会丢失结构信息。我们构建三级动量特征:
def build_momentum_features(df: pd.DataFrame) -> pd.DataFrame: """ 构建周/日/分钟三级动量特征,捕捉不同时间尺度的市场惯性 :param df: 输入DataFrame,index为datetime,必须含'close'列 :return: 新增6个动量特征列 """ df = df.copy() # 1. 周线动量(5日,代表一周交易日) df['mom_week'] = df['close'].pct_change(5) # 2. 日线动量(20日,代表一个月) df['mom_month'] = df['close'].pct_change(20) # 3. 分钟线动量(需分钟数据,此处用日内波动率代理) # 计算日波动率:(high-low)/open df['daily_volatility'] = (df['high'] - df['low']) / df['open'] # 过去5日平均波动率 df['vol_avg_5d'] = df['daily_volatility'].rolling(5).mean() # 波动率动量:当前波动率 vs 5日均值 df['vol_mom'] = df['daily_volatility'] / df['vol_avg_5d'] - 1 # 4. 动量斜率:用线性回归拟合过去20日价格趋势的斜率 def calc_slope(series): if len(series) < 10: return 0 x = np.arange(len(series)) slope, _ = np.polyfit(x, series, 1) return slope df['mom_slope_20d'] = df['close'].rolling(20).apply(calc_slope, raw=True) # 5. 动量背离检测:价格创新高但动量未创新高 df['price_high_20d'] = df['close'].rolling(20).max() df['mom_high_20d'] = df['mom_month'].rolling(20).max() df['momentum_divergence'] = ( (df['close'] == df['price_high_20d']) & (df['mom_month'] < df['mom_high_20d'] * 0.95) # 动量弱于前高95% ).astype(int) # 6. 动量衰减率:当前动量 / 5日前动量 df['mom_decay'] = df['mom_month'] / df['mom_month'].shift(5) return df # 应用特征构建 df_features = build_momentum_features(df_final)为什么这样设计?
mom_week和mom_month不是简单叠加,而是正交化:周线过滤噪音,月线捕捉趋势,二者比值(mom_month / mom_week)可作趋势强度指标;vol_mom解决“放量上涨”与“缩量上涨”的语义区分,这是技术分析的核心判据;momentum_divergence直接编码“顶背离”这一经典卖出信号,比单纯看RSI超买更可靠;- 所有特征均通过
rolling().apply()实现,避免pct_change()在停牌日产生的虚假跳变。
4.2 处理前视偏差的终极校验:滚动窗口必须严格对齐交易日历
前视偏差(Look-Ahead Bias)是量化回测死亡之吻。常见错误包括:用df.rolling(20).mean()计算均线,却未排除停牌日;用df['pe'].shift(-1)预测明日PE,却不知PE是季度发布、滞后两个月。真正的滚动窗口,必须基于真实交易日历:
import exchange_calendars as mcal def get_trading_calendar(exchange: str = "XSHG") -> pd.DatetimeIndex: """ 获取指定交易所交易日历(A股用XSHG,美股用XNYS) :param exchange: 交易所代码 :return: DatetimeIndex,包含所有交易日 """ cal = mcal.get_calendar(exchange) # 获取最近3年的交易日 start_date = (pd.Timestamp.now() - pd.DateOffset(years=3)).strftime('%Y-%m-%d') end_date = pd.Timestamp.now().strftime('%Y-%m-%d') schedule = cal.schedule(start_date=start_date, end_date=end_date) return schedule.index def safe_rolling_mean(df: pd.DataFrame, window: int, column: str, calendar: pd.DatetimeIndex) -> pd.Series: """ 基于真实交易日历的滚动均值,严格排除停牌日 :param df: 原始DataFrame,index为datetime :param window: 窗口大小(交易日数) :param column: 待计算列名 :param calendar: 交易日历DatetimeIndex :return: 滚动均值Series,index与df一致 """ # 将df index与calendar对齐,缺失日补NaN df_aligned = df.reindex(calendar, method='ffill') # 用前值填充非交易日 # 在对齐后的数据上计算滚动均值 rolling_series = df_aligned[column].rolling(window=window, min_periods=window).mean() # 再次reindex回原始df index,确保长度一致 return rolling_series.reindex(df.index) # 使用示例 trading_days = get_trading_calendar("XSHG") df_features['ma20_safe'] = safe_rolling_mean(df_features, 20, 'close', trading_days)避坑原理:
exchange_calendars库内置上交所、深交所、纳斯达克等全球主要交易所日历,get_calendar("XSHG").schedule()返回的是精确到秒的开盘/收盘时间,比手动维护节假日列表可靠百倍。reindex(calendar, method='ffill')确保计算时只用真实交易日数据,彻底杜绝“用周末数据平滑周一价格”的低级错误。
5. 回测不是画曲线,而是用订单簿快照重建成交执行质量
90%的“完美回测”在实盘崩溃,根源在于:回测引擎把成交价当成close,而真实市场中,你的买单可能以ask+0.01成交,卖单以bid-0.01成交。没有订单簿(Order Book)模拟,回测就是空中楼阁。我们不用复杂框架,用backtrader轻量级定制,核心是重写Broker的exec_buy方法:
5.1 构建简易订单簿模拟器:用买卖盘口数据注入滑点
import backtrader as bt class OrderBookBroker(bt.brokers.BackBroker): """ 基于买卖盘口的Broker,模拟真实成交滑点 """ params = ( ('slippage_pct', 0.001), # 默认滑点0.1% ('min_volume', 100), # 最小委托量 ) def exec_buy(self, order, price, size): """ 重写买入执行逻辑:优先吃单,不足部分按盘口价格成交 """ # 获取当前买卖盘口(此处简化为从data中读取bid/ask列) # 实际中应从实时行情API获取,demo中用历史数据近似 bid = self.data.bid[0] if hasattr(self.data, 'bid') else self.data.close[0] ask = self.data.ask[0] if hasattr(self.data, 'ask') else self.data.close[0] # 计算目标成交价:在ask基础上加滑点 target_price = ask * (1 + self.p.slippage_pct) # 模拟撮合:若委托量小,直接吃单;若大,则分笔成交 if size <= self.p.min_volume: executed_price = target_price else: # 大单分笔:前50%按ask成交,后50%按ask*1.001成交 executed_price = ask * 0.5 + target_price * 0.5 # 更新订单状态 order.executed.price = executed_price order.executed.value = executed_price * size order.executed.comm = 0 # 手续费另计 order.executed.pnl = 0 # 盈亏由策略计算 return True # 在Cerebro中使用 cerebro = bt.Cerebro() cerebro.broker = OrderBookBroker() # 加载数据时,需包含bid/ask列(可从tick数据聚合) data = bt.feeds.PandasData( dataname=df_features, openinterest=None, # 若原始数据无bid/ask,用close±0.5%近似 bid=df_features['close'] * 0.995, ask=df_features['close'] * 1.005 ) cerebro.adddata(data)参数说明:
slippage_pct=0.001对应0.1%滑点,A股万2.5佣金下,0.1%滑点已覆盖手续费+冲击成本;min_volume=100是A股最小交易单位(1手=100股)。这个Broker不追求完美模拟,而是用可解释的规则注入市场摩擦——让回测曲线不再光滑,逼你直面真实世界的执行损耗。
5.2 回测结果归因:不只是看夏普比率,要看每笔交易的盈亏来源
回测报告不能只输出sharpe=2.3,而要回答:“盈利来自趋势跟踪?还是反转套利?或是波动率捕获?” 我们用bt.analyzers.TradeAnalyzer深度解析:
def analyze_trades(cerebro: bt.Cerebro): """ 深度交易归因分析,输出每类交易的贡献度 """ # 添加分析器 cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='ta') cerebro.addanalyzer(bt.analyzers.SQN, _name='sqn') cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown') results = cerebro.run() strat = results[0] # 提取交易记录 trade_analyzer = strat.analyzers.ta.get_analysis() # 计算各类交易胜率与盈亏比 total_closed = trade_analyzer.total.closed won = trade_analyzer.won.total lost = trade_analyzer.lost.total win_rate = won / total_closed if total_closed > 0 else 0 # 平均盈利/平均亏损 avg_win = trade_analyzer.won.pnl.total / won if won > 0 else 0 avg_loss = trade_analyzer.lost.pnl.total / lost if lost > 0 else 0 profit_factor = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf') # 按持仓时间归因:短线(<3天)、中线(3-20天)、长线(>20天) short_trades = [t for t in trade_analyzer.trades if t.len < 3] mid_trades = [t for t in trade_analyzer.trades if 3 <= t.len <= 20] long_trades = [t for t in trade_analyzer.trades if t.len > 20] print(f"=== 交易归因报告 ===") print(f"总交易数: {total_closed}, 胜率: {win_rate:.2%}") print(f"盈亏比: {profit_factor:.2f} (平均盈利{avg_win:.2f} / 平均亏损{abs(avg_loss):.2f})") print(f"短线贡献: {sum(t.pnl for t in short_trades):.2f} ({len(short_trades)}笔)") print(f"中线贡献: {sum(t.pnl for t in mid_trades):.2f} ({len(mid_trades)}笔)") print(f"长线贡献: {sum(t.pnl for t in long_trades):.2f} ({len(long_trades)}笔)") return trade_analyzer # 运行归因分析 analyzer = analyze_trades(cerebro)为什么必须做归因?
- 如果80%盈利来自长线交易,但你的策略信号90%是日内触发,说明信号与持仓不匹配,需调整止盈逻辑;
- 如果短线交易胜率70%但盈亏比仅1.2,而中线胜率40%但盈亏比5.0,说明策略本质是中线趋势,应关闭短线信号;
- 这些结论无法从净值曲线看出,必须穿透到每一笔交易的
pnl、len、pnlcomm字段。
6. 部署不是“导出模型”,而是把信号变成可审计、可追溯、可熔断的生产指令
当你在Jupyter里跑通回测,下一步不是打包成exe,而是思考:这个信号如何进入交易系统?谁来审核?异常时如何熔断?历史信号能否回溯验证?这才是“全流程”的终点。我过去踩过的最大坑,是把model.predict(X)的输出直接喂给下单接口,结果某天特征工程里一个fillna(0)把停牌日的volume填成0,模型误判为“巨量启动”,自动扫货10万股——而整个过程没有任何人工干预点。
6.1 信号生成服务化:用FastAPI暴露REST接口,强制输入校验
from fastapi import FastAPI, HTTPException, Depends from pydantic import BaseModel import joblib import pandas as pd from datetime import datetime app = FastAPI(title="金融信号服务") # 加载训练好的模型和预处理器 model = joblib.load("models/lstm_model.pkl") scaler = joblib.load("models/scaler.pkl") class SignalRequest(BaseModel): symbol: str date: str # YYYY-MM-DD features: dict # {"mom_month": 0.12, "vol_mom": 1.3, ...} @app.post("/generate_signal") def generate_signal(request: SignalRequest): """ 生成交易信号,强制校验输入合法性 """ # 1. 校验日期是否为交易日 try: dt = datetime.strptime(request.date, "%Y-%m-%d") if dt.weekday() >= 5: # 周六日 raise HTTPException(status_code=400, detail="非交易日") except ValueError: raise HTTPException(status_code=400, detail="日期格式错误") # 2. 校验特征完整性 required_features = ["mom_month", "vol_mom", "momentum_divergence"] missing = [f for f in required_features if f not in request.features] if missing: raise HTTPException(status_code=400, detail=f"缺少必要特征: {missing}") # 3. 校验特征值范围(防异常值污染模型) for feat, val in request.features.items(): if not isinstance(val, (int, float)): raise HTTPException(status_code=400, detail=f"{feat} 必须为数值") if abs(val) > 1000: # 设定合理阈值 raise HTTPException(status_code=400, detail=f"{feat} 数值异常: {val}") # 4. 构造输入向量 X = pd.DataFrame([request.features]) X_scaled = scaler.transform(X) # 5. 模型预测 try: pred = model.predict(X_scaled)[0] signal = "BUY" if pred > 0.5 else "SELL" if pred < 0.3 else "HOLD" # 6. 生成审计日志(写入数据库或文件) log_entry = { "timestamp": datetime.now().isoformat(), "symbol": request.symbol, "date": request.date, "input_features": request.features, "model_output": float(pred), "signal": signal, "version": "v1.2.0" # 模型版本,便于回溯 } # 此处写入审计日志(略) return {"signal": signal, "confidence": float(pred), "audit_id": "log_abc123"} except Exception as e: # 模型异常时,返回安全默认值 return {"signal": "HOLD", "confidence": 0.0, "error": str(e)} # 启动服务:uvicorn main:app --reload关键设计:这个API不是简单包装
predict(),而是四重防护:① 交易日校验,堵住周末信号漏洞;② 特征完整性检查,防止前端传参缺失;③ 数值范围校验,拦截传感器故障导致的异常值;④ 审计日志强制写入,确保每条信号可追溯。**没有审计日志的信号服务,等于没有刹车的
本文还有配套的精品资源,点击获取