# -*- coding: utf-8 -*- """ 希希量化分析系统 - 因子表达式引擎 基于QuantGPT expression_parser.py适配,去除Rust依赖和外部模块引用 支持60+算子:时序、截面、技术指标、基本面、算术/比较/逻辑运算、三元运算符 """ import numpy as np import pandas as pd import re import warnings warnings.filterwarnings('ignore') # ================================================================ # 允许的基本面列名(替代QuantGPT的fundamental_data模块) # ================================================================ _FUNDAMENTAL_COLUMNS = { # 估值 'pe', 'pe_ttm', 'pb', 'ps', 'ps_ttm', 'pcf', 'pcf_ttm', 'ev_ebitda', # 盈利能力 'roe', 'roa', 'gross_margin', 'net_margin', 'ebitda_margin', 'operating_margin', 'ebit_margin', # 成长性 'revenue_yoy', 'net_profit_yoy', 'eps_yoy', 'operating_revenue_yoy', 'revenue_qoq', 'net_profit_qoq', # 每股指标 'eps', 'eps_ttm', 'bvps', 'dps', 'operating_cashflow_ps', # 资产负债 'debt_ratio', 'current_ratio', 'quick_ratio', 'asset_turnover', 'equity_multiplier', 'cash_ratio', # 现金流 'operating_cashflow', 'free_cashflow', 'cashflow_to_debt', 'fcf_yoy', 'ocf_yoy', # 其他 'market_cap', 'float_cap', 'total_shares', 'float_shares', 'dividend_yield', 'retention_ratio', 'payout_ratio', 'institutional_ratio', 'north_flow', 'main_net_inflow', 'volume_ratio', 'amplitude', } # 价格/量相关列 _PRICE_COLUMNS = { 'open', 'high', 'low', 'close', 'volume', 'amount', 'pre_close', 'turnover', 'turnover_rate', 'change_pct', 'vwap', 'adj_factor', } # 所有允许的列名 ALLOWED_COLUMNS = _FUNDAMENTAL_COLUMNS | _PRICE_COLUMNS # Alpha101 别名映射 _ALPHA101_ALIASES = { 'delta': 'ts_delta', 'delay': 'ts_shift', 'correlation': 'ts_corr', 'covariance': 'ts_cov', 'rank': 'ts_rank', 'scale': 'ts_scale', 'signed_power': 'ts_signed_power', 'product': 'ts_product', 'stddev': 'ts_std', 'log': 'ts_log', 'sum': 'ts_sum', 'adv': 'ts_mean', 'min': 'ts_min', 'max': 'ts_max', 'argmax': 'ts_argmax', 'argmin': 'ts_argmin', 'av_diff': 'ts_av_diff', 'mean': 'ts_mean', 'indneutralize': 'group_neutralize', 'ts_decay_linear': 'decay_linear', 'ts_std_dev': 'ts_std', 'ts_regression': 'ts_corr', } class ExpressionParser: """因子表达式解析引擎 - 支持嵌套运算、时序/截面算子""" def __init__(self): self._operators = { # 时序算子 'ts_mean': self._calc_ts_mean, 'ts_std': self._calc_ts_std, 'ts_delta': self._calc_ts_delta, 'ts_rank': self._calc_ts_rank, 'ts_argmax': self._calc_ts_argmax, 'ts_argmin': self._calc_ts_argmin, 'ts_corr': self._calc_ts_corr, 'ts_cov': self._calc_ts_cov, 'decay_linear': self._calc_decay_linear, 'product': self._calc_product, 'ts_av_diff': self._calc_ts_av_diff, 'ts_zscore': self._calc_ts_zscore, 'ts_sum': self._calc_ts_sum, 'ts_min': self._calc_ts_min, 'ts_max': self._calc_ts_max, 'ts_shift': self._calc_ts_shift, 'ts_skewness': self._calc_ts_skewness, 'ts_kurtosis': self._calc_ts_kurtosis, 'ts_median': self._calc_ts_median, 'ts_quantile': self._calc_ts_quantile, 'ts_log': self._calc_ts_log, 'ts_abs': self._calc_ts_abs, 'ts_sign': self._calc_ts_sign, 'ts_pow': self._calc_ts_pow, 'ts_prod': self._calc_product, 'ts_co_kurtosis': lambda f, w: f, # 截面算子 'rank': self._calc_cross_rank, 'zscore': self._calc_cross_zscore, 'scale': self._calc_cross_scale, 'group_rank': self._calc_group_rank, 'group_zscore': self._calc_group_zscore, 'group_neutralize': self._calc_group_neutralize, # 技术指标 'ema': self._calc_ema, 'sma': self._calc_sma, 'wma': self._calc_wma, 'rsi': self._calc_rsi, 'macd': self._calc_macd, 'obv': self._calc_obv, 'atr': self._calc_atr, 'boll_upper': self._calc_boll_upper, 'boll_lower': self._calc_boll_lower, 'boll_mid': self._calc_boll_mid, 'kdj_k': self._calc_kdj_k, 'kdj_d': self._calc_kdj_d, 'wr': self._calc_wr, 'cci': self._calc_cci, # Alpha101别名已在上层解析时替换 } def parse(self, expression, data, group_col=None): """ 解析并计算因子表达式 Parameters ---------- expression : str 因子表达式,如 "ts_rank(ts_delta(close, 5), 20)" data : pd.DataFrame 包含价格/基本面列的DataFrame,columns中须有表达式引用的列 group_col : str, optional 分组列名(用于group_rank等算子) Returns ------- pd.Series : 计算结果 """ # 1. 替换Alpha101别名 expr = self._replace_aliases(expression) # 2. 替换三元运算符 a ? b : c → (a) if (a, b, c) expr = self._convert_ternary_operators(expr) # 3. 解析并计算 result = self._evaluate(expr, data, group_col) return result def parse_expression(expression, data, group_col=None): """便捷函数""" parser = ExpressionParser() return parser.parse(expression, data, group_col) # ──── 别名替换 ──── def _replace_aliases(self, expr): for alias, target in _ALPHA101_ALIASES.items(): pattern = rf'\b{alias}\s*\(' expr = re.sub(pattern, f'{target}(', expr) return expr # ──── 三元运算符转换 ──── def _convert_ternary_operators(self, expr): """将 a ? b : c 转换为嵌套函数调用""" while '?' in expr: # 找到最内层的 ?...:... idx = expr.rfind('?') colon_idx = expr.find(':', idx) if colon_idx == -1: break condition = expr[:idx].rstrip() true_val = expr[idx+1:colon_idx].strip() false_val = expr[colon_idx+1:].strip() expr = f'ternary({condition},{true_val},{false_val})' return expr # ──── 核心解析器 ──── def _evaluate(self, expr, data, group_col=None): """递归解析表达式树""" expr = expr.strip() # 括号包裹的表达式 if expr.startswith('(') and self._find_matching_paren(expr, 0) == len(expr) - 1: return self._evaluate(expr[1:-1], data, group_col) # 函数调用 match = re.match(r'^(\w+)\s*\(', expr) if match: func_name = match.group(1) left_paren_pos = match.end() - 1 # 左括号位置 # 用_find_matching_paren从左括号位置找匹配的右括号 paren_end = self._find_matching_paren(expr, left_paren_pos) if paren_end is not None and paren_end >= 0: args_str = expr[left_paren_pos+1:paren_end] else: # 退化处理 args_str = expr[match.end():] if args_str.endswith(')'): args_str = args_str[:-1] args = self._split_args(args_str) return self._call_function(func_name, args, data, group_col) # 算术/比较/逻辑运算符(从低优先级开始解析) for op in ['&&', '||']: idx = self._find_operator(expr, op, data) if idx is not None: left = self._evaluate(expr[:idx], data, group_col) right = self._evaluate(expr[idx+len(op):], data, group_col) if op == '&&': return left.astype(float) * right.astype(float) else: return (left.astype(float) + right.astype(float)).clip(upper=1) for op in ['>=', '<=', '!=', '==', '>', '<']: idx = self._find_operator(expr, op, data) if idx is not None: left = self._evaluate(expr[:idx], data, group_col) right = self._evaluate(expr[idx+len(op):], data, group_col) if op == '>': return (left > right).astype(float) elif op == '<': return (left < right).astype(float) elif op == '>=': return (left >= right).astype(float) elif op == '<=': return (left <= right).astype(float) elif op == '==': return (left == right).astype(float) elif op == '!=': return (left != right).astype(float) for op in ['+', '-']: idx = self._find_operator(expr, op, data) if idx is not None and idx > 0: left = self._evaluate(expr[:idx], data, group_col) right = self._evaluate(expr[idx+1:], data, group_col) if op == '+': return left.astype(float) + right.astype(float) else: return left.astype(float) - right.astype(float) for op in ['*', '/']: idx = self._find_operator(expr, op, data) if idx is not None and idx > 0: left = self._evaluate(expr[:idx], data, group_col) right = self._evaluate(expr[idx+1:], data, group_col) if op == '*': return left.astype(float) * right.astype(float) else: return left.astype(float) / right.astype(float).replace(0, np.nan) # 一元运算符 if expr.startswith('-'): return -self._evaluate(expr[1:], data, group_col) if expr.startswith('!'): return (1 - self._evaluate(expr[1:], data, group_col)).astype(float) if expr.startswith('abs(') or expr.startswith('Abs('): inner = expr[4:-1] if expr.endswith(')') else expr[4:] return self._evaluate(inner, data, group_col).abs() if expr.startswith('log('): inner = expr[4:-1] if expr.endswith(')') else expr[4:] return np.log(self._evaluate(inner, data, group_col).clip(lower=1e-10)) # 特殊变量 if expr == 'ternary': return pd.Series(0, index=data.index) # 列名 if expr in data.columns: return data[expr].astype(float) # 数值 try: return pd.Series(float(expr), index=data.index) except ValueError: pass raise ValueError(f"无法解析表达式: {expr}") def _find_matching_paren(self, expr, start): """找到与start位置左括号匹配的右括号位置""" depth = 0 for i in range(start, len(expr)): if expr[i] == '(': depth += 1 elif expr[i] == ')': depth -= 1 if depth == 0: return i return -1 def _split_args(self, args_str): """按逗号分割参数(尊重嵌套括号)""" args = [] depth = 0 current = '' for ch in args_str: if ch == '(': depth += 1 current += ch elif ch == ')': depth -= 1 current += ch elif ch == ',' and depth == 0: args.append(current.strip()) current = '' else: current += ch if current.strip(): args.append(current.strip()) return args def _find_operator(self, expr, op, data): """找到非括号内的运算符位置""" depth = 0 in_func = False for i in range(len(expr)): ch = expr[i] if ch == '(': depth += 1 elif ch == ')': depth -= 1 elif depth == 0: if expr[i:i+len(op)] == op: # 确保不是函数名的一部分 if i > 0 and expr[i-1].isalpha(): continue if i + len(op) < len(expr) and expr[i+len(op)].isalpha(): continue return i return None def _call_function(self, func_name, args, data, group_col): """调用算子函数""" if func_name == 'ternary': if len(args) == 3: cond = self._evaluate(args[0], data, group_col) true_v = self._evaluate(args[1], data, group_col) false_v = self._evaluate(args[2], data, group_col) return np.where(cond > 0, true_v, false_v) raise ValueError("ternary需要3个参数: condition, true_val, false_val") if func_name == 'group_rank' and group_col and group_col not in data.columns: args_eval = [self._evaluate(a, data, group_col) for a in args] return args_eval[0] if len(args_eval) == 1 else pd.concat(args_eval, axis=1).iloc[:, -1] if func_name in self._operators: args_eval = [] for a in args: a = a.strip() # 如果参数是纯数字,直接转float try: args_eval.append(float(a)) continue except (ValueError, TypeError): pass # 否则作为表达式解析 try: v = self._evaluate(a, data, group_col) # 如果结果是长度为1的Series,取标量 if isinstance(v, pd.Series) and len(v) == 1: v = float(v.iloc[0]) args_eval.append(v) except (ValueError, KeyError) as e: args_eval.append(a) # 处理group_col参数 gc = group_col if func_name.startswith('group_') else None try: return self._operators[func_name](*args_eval, group_col=gc) except TypeError as e: # 可能不需要group_col参数 try: return self._operators[func_name](*args_eval) except TypeError: raise ValueError(f"算子 {func_name} 调用失败: {e}, 参数: {args_eval}") raise ValueError(f"未知算子: {func_name}") # ================================================================ # 时序算子 (Time-Series Operators) # ================================================================ def _calc_ts_mean(self, x, window, **kw): if isinstance(window, (int, float)): return x.rolling(int(window), min_periods=1).mean() return x.mean() def _calc_ts_std(self, x, window, **kw): if isinstance(window, (int, float)): return x.rolling(int(window), min_periods=1).std() return x.std() def _calc_ts_delta(self, x, period, **kw): if isinstance(period, (int, float)): return x - x.shift(int(period)) return x.diff() def _calc_ts_rank(self, x, window, **kw): if isinstance(window, (int, float)): return x.rolling(int(window), min_periods=1).apply( lambda s: pd.Series(s).rank(pct=True).iloc[-1] if len(s) > 1 else 0.5, raw=False) return x.rank(pct=True) def _calc_ts_argmax(self, x, window, **kw): if isinstance(window, (int, float)): return x.rolling(int(window), min_periods=1).apply( lambda s: np.argmax(s.values) if len(s) > 0 else 0, raw=True) return pd.Series(np.argmax(x.values), index=x.index) def _calc_ts_argmin(self, x, window, **kw): if isinstance(window, (int, float)): return x.rolling(int(window), min_periods=1).apply( lambda s: np.argmin(s.values) if len(s) > 0 else 0, raw=True) return pd.Series(np.argmin(x.values), index=x.index) def _calc_ts_corr(self, x, y, window, **kw): if isinstance(window, (int, float)): df = pd.concat([x, y], axis=1) return df.iloc[:,0].rolling(int(window), min_periods=2).corr(df.iloc[:,1]) return x.corr(y) def _calc_ts_cov(self, x, y, window, **kw): if isinstance(window, (int, float)): df = pd.concat([x, y], axis=1) return df.iloc[:,0].rolling(int(window), min_periods=2).cov(df.iloc[:,1]) return x.cov(y) def _calc_decay_linear(self, x, window, **kw): if isinstance(window, (int, float)): w = int(window) def weighted_mean(s): weights = np.arange(1, len(s)+1, dtype=float) weights /= weights.sum() return np.nansum(s.values * weights) return x.rolling(w, min_periods=1).apply(weighted_mean, raw=False) return x def _calc_product(self, x, window, **kw): if isinstance(window, (int, float)): return x.rolling(int(window), min_periods=1).apply( lambda s: np.nanprod(s.clip(lower=-1e10, upper=1e10).values), raw=True) return x.cumprod() def _calc_ts_av_diff(self, x, window, **kw): mean_val = x.rolling(int(window), min_periods=1).mean() return x - mean_val def _calc_ts_zscore(self, x, window, **kw): if isinstance(window, (int, float)): m = x.rolling(int(window), min_periods=1).mean() s = x.rolling(int(window), min_periods=1).std() return (x - m) / s.replace(0, np.nan) return (x - x.mean()) / x.std().replace(0, np.nan) def _calc_ts_sum(self, x, window, **kw): return x.rolling(int(window), min_periods=1).sum() def _calc_ts_min(self, x, window, **kw): return x.rolling(int(window), min_periods=1).min() def _calc_ts_max(self, x, window, **kw): return x.rolling(int(window), min_periods=1).max() def _calc_ts_shift(self, x, period, **kw): return x.shift(int(period)) def _calc_ts_skewness(self, x, window, **kw): return x.rolling(int(window), min_periods=3).skew() def _calc_ts_kurtosis(self, x, window, **kw): return x.rolling(int(window), min_periods=4).kurt() def _calc_ts_median(self, x, window, **kw): return x.rolling(int(window), min_periods=1).median() def _calc_ts_quantile(self, x, window, q=0.5, **kw): return x.rolling(int(window), min_periods=1).quantile(float(q)) def _calc_ts_log(self, x, **kw): return np.log(x.clip(lower=1e-10)) def _calc_ts_abs(self, x, **kw): return x.abs() def _calc_ts_sign(self, x, **kw): return np.sign(x) def _calc_ts_pow(self, x, power, **kw): return x ** float(power) # ================================================================ # 截面算子 (Cross-Sectional Operators) # ================================================================ def _calc_cross_rank(self, x, **kw): return x.rank(pct=True) def _calc_cross_zscore(self, x, **kw): m = x.mean() s = x.std() return (x - m) / s if s > 0 else pd.Series(0, index=x.index) def _calc_cross_scale(self, x, **kw): s = x.abs().sum() return x / s if s > 0 else pd.Series(0, index=x.index) def _calc_group_rank(self, x, group_col=None, **kw): if group_col is not None and hasattr(x, 'index'): # 尝试从data的index获取分组 return x.rank(pct=True) return x.rank(pct=True) def _calc_group_zscore(self, x, group_col=None, **kw): return self._calc_cross_zscore(x) def _calc_group_neutralize(self, x, group_col=None, **kw): return self._calc_cross_zscore(x) # ================================================================ # 技术指标 (Technical Indicators) # ================================================================ def _calc_ema(self, x, period, **kw): return x.ewm(span=int(period), adjust=False).mean() def _calc_sma(self, x, period, **kw): return x.rolling(int(period), min_periods=1).mean() def _calc_wma(self, x, period, **kw): w = int(period) def weighted_avg(s): weights = np.arange(1, len(s)+1, dtype=float) return np.nansum(s.values * weights) / weights.sum() return x.rolling(w, min_periods=1).apply(weighted_avg, raw=False) def _calc_rsi(self, x, period=14, **kw): delta = x.diff() gain = delta.where(delta > 0, 0) loss = (-delta).where(delta < 0, 0) avg_gain = gain.ewm(alpha=1.0/int(period), min_periods=int(period)).mean() avg_loss = loss.ewm(alpha=1.0/int(period), min_periods=int(period)).mean() rs = avg_gain / avg_loss.replace(0, 1e-10) return 100 - (100 / (1 + rs)) def _calc_macd(self, x, fast=12, slow=26, signal=9, **kw): ema_fast = x.ewm(span=int(fast), adjust=False).mean() ema_slow = x.ewm(span=int(slow), adjust=False).mean() dif = ema_fast - ema_slow dea = dif.ewm(span=int(signal), adjust=False).mean() histogram = (dif - dea) * 2 return dif def _calc_obv(self, x, volume=None, **kw): if volume is not None: return x * volume return x def _calc_atr(self, x, period=14, **kw): # 简化:用close的rolling范围近似 return x.rolling(int(period), min_periods=1).apply( lambda s: s.max() - s.min(), raw=True) def _calc_boll_upper(self, x, period=20, num_std=2, **kw): mid = x.rolling(int(period), min_periods=1).mean() std = x.rolling(int(period), min_periods=1).std() return mid + float(num_std) * std def _calc_boll_lower(self, x, period=20, num_std=2, **kw): mid = x.rolling(int(period), min_periods=1).mean() std = x.rolling(int(period), min_periods=1).std() return mid - float(num_std) * std def _calc_boll_mid(self, x, period=20, **kw): return x.rolling(int(period), min_periods=1).mean() def _calc_kdj_k(self, x, period=9, **kw): lowest = x.rolling(int(period), min_periods=1).min() highest = x.rolling(int(period), min_periods=1).max() return (x - lowest) / (highest - lowest).replace(0, np.nan) * 100 def _calc_kdj_d(self, x, period=9, m=3, **kw): k = self._calc_kdj_k(x, period) return k.rolling(int(m), min_periods=1).mean() def _calc_wr(self, x, period=14, **kw): highest = x.rolling(int(period), min_periods=1).max() lowest = x.rolling(int(period), min_periods=1).min() return (highest - x) / (highest - lowest).replace(0, np.nan) * -100 def _calc_cci(self, x, period=20, **kw): tp = x # simplified: use close as typical price ma = tp.rolling(int(period), min_periods=1).mean() md = tp.rolling(int(period), min_periods=1).apply( lambda s: np.abs(s - s.mean()).mean(), raw=True) return (tp - ma) / (0.015 * md.replace(0, np.nan)) # 便捷函数 def parse_expression(expression, data, group_col=None): """ 解析因子表达式(全局便捷入口) Parameters ---------- expression : str - 因子表达式 data : pd.DataFrame - 包含所需列的数据 group_col : str, optional - 分组列名 Returns ------- pd.Series 示例: >>> parse_expression("ts_rank(ts_delta(close, 5), 20)", df) >>> parse_expression("rank(rsi(close, 14))", df) >>> parse_expression("ts_zscore(close / ts_mean(close, 20), 60)", df) >>> parse_expression("close > ts_mean(close, 20) ? 1 : -1", df) """ parser = ExpressionParser() return parser.parse(expression, data, group_col) # 验证表达式语法 def validate_expression(expression): """ 验证表达式是否包含允许的算子和列名 返回 (is_valid, error_message) """ try: parser = ExpressionParser() # 替换别名 expr = parser._replace_aliases(expression) expr = parser._convert_ternary_operators(expr) # 检查是否有未知函数调用 func_calls = re.findall(r'(\w+)\s*\(', expr) known_ops = set(parser._operators.keys()) | {'ternary'} for fc in func_calls: if fc not in known_ops and fc not in ALLOWED_COLUMNS: return False, f"未知算子或列名: {fc}" return True, None except Exception as e: return False, str(e) # 常用因子模板 FACTOR_TEMPLATES = { 'momentum_5d': {'expr': 'ts_delta(close, 5) / ts_delay(close, 5)', 'name': '5日动量', 'category': '动量'}, 'momentum_20d': {'expr': 'ts_delta(close, 20) / ts_delay(close, 20)', 'name': '20日动量', 'category': '动量'}, 'volatility_20d': {'expr': 'ts_std(close/ts_delay(close,1) - 1, 20)', 'name': '20日波动率', 'category': '波动率'}, 'rsi_14': {'expr': 'rsi(close, 14)', 'name': 'RSI(14)', 'category': '技术'}, 'turnover_rate': {'expr': 'ts_mean(volume, 5)', 'name': '5日均量', 'category': '量价'}, 'boll_deviation': {'expr': '(close - boll_mid(close, 20)) / ts_std(close, 20)', 'name': '布林偏离度', 'category': '技术'}, 'price_momentum_rank': {'expr': 'ts_rank(ts_delta(close, 10) / ts_delay(close, 10), 60)', 'name': '价格动量排名', 'category': '动量'}, 'volume_price_corr': {'expr': 'ts_corr(close, volume, 20)', 'name': '量价相关系数', 'category': '量价'}, 'mean_reversion_5d': {'expr': '-ts_delta(close, 5) / ts_delay(close, 5)', 'name': '5日均值回归', 'category': '反转'}, 'high_low_ratio': {'expr': 'ts_mean(close/ts_delay(close,1) - 1, 5) / ts_std(close/ts_delay(close,1) - 1, 20)', 'name': '高低比率', 'category': '风险'}, 'ema_cross': {'expr': 'ema(close, 12) - ema(close, 26)', 'name': 'EMA交叉', 'category': '趋势'}, 'kurtosis_20d': {'expr': 'ts_kurtosis(close/ts_delay(close,1) - 1, 20)', 'name': '20日峰度', 'category': '分布'}, 'skewness_20d': {'expr': 'ts_skewness(close/ts_delay(close,1) - 1, 20)', 'name': '20日偏度', 'category': '分布'}, 'decay_volatility': {'expr': 'decay_linear(ts_std(close/ts_delay(close,1) - 1, 5), 20)', 'name': '衰减波动率', 'category': '波动率'}, 'zscore_60d': {'expr': 'ts_zscore(close, 60)', 'name': '60日Z-Score', 'category': '统计'}, 'cumulative_return_20d': {'expr': 'product(1 + (close - ts_delay(close, 1))/ts_delay(close, 1), 20)', 'name': '20日累计收益', 'category': '动量'}, 'max_drawdown_20d': {'expr': 'ts_min(close/ts_delay(close,1) - 1, 20)', 'name': '20日最大回撤', 'category': '风险'}, 'turnover_acceleration': {'expr': 'ts_delta(volume, 5) - ts_delta(volume, 20)', 'name': '换手加速度', 'category': '量价'}, }