macro.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. """
  2. 宏观生态识别器
  3. 识别市场的"季节":春季(复苏)、夏季(繁荣)、秋季(分化)、冬季(萧条)
  4. """
  5. from dataclasses import dataclass
  6. from typing import Dict, List, Optional, Tuple
  7. from enum import Enum
  8. import numpy as np
  9. import pandas as pd
  10. class MacroRegime(Enum):
  11. """宏观生态类型"""
  12. SPRING = "spring" # 春季(复苏)
  13. SUMMER = "summer" # 夏季(繁荣)
  14. AUTUMN = "autumn" # 秋季(分化)
  15. WINTER = "winter" # 冬季(萧条)
  16. UNKNOWN = "unknown" # 未知/过渡
  17. @dataclass
  18. class MacroEcosystem:
  19. """宏观生态数据结构"""
  20. regime: MacroRegime
  21. confidence: float
  22. volatility_trend: float # 波动率趋势
  23. volume_trend: float # 成交量趋势
  24. dispersion: float # 板块离散度
  25. adx_value: float # ADX值
  26. description: str
  27. class MacroEcosystemIdentifier:
  28. """
  29. 宏观生态识别器
  30. 基于以下因子识别市场季节:
  31. 1. 波动率期限结构(近月波动率变化)
  32. 2. 板块离散度(成分股收益率标准差的历史分位)
  33. 3. 资金流向广度(涨跌家数比的熵值)
  34. 4. ADX趋势强度
  35. """
  36. def __init__(
  37. self,
  38. lookback_days: int = 252,
  39. spring_vol_rebound: float = 0.20,
  40. spring_volume_ratio: float = 1.2,
  41. summer_adx_threshold: float = 25,
  42. summer_adx_duration: int = 5,
  43. autumn_dispersion_pct: float = 70,
  44. winter_volume_pct: float = 50,
  45. winter_vol_compression: float = 20
  46. ):
  47. self.lookback_days = lookback_days
  48. self.spring_vol_rebound = spring_vol_rebound
  49. self.spring_volume_ratio = spring_volume_ratio
  50. self.summer_adx_threshold = summer_adx_threshold
  51. self.summer_adx_duration = summer_adx_duration
  52. self.autumn_dispersion_pct = autumn_dispersion_pct
  53. self.winter_volume_pct = winter_volume_pct
  54. self.winter_vol_compression = winter_vol_compression
  55. def identify(
  56. self,
  57. price_data: pd.DataFrame,
  58. sector_data: Optional[pd.DataFrame] = None
  59. ) -> MacroEcosystem:
  60. """
  61. 识别当前宏观生态
  62. Args:
  63. price_data: 价格数据,包含 ['open', 'high', 'low', 'close', 'volume']
  64. sector_data: 板块数据(可选),成分股收益率数据
  65. Returns:
  66. MacroEcosystem: 宏观生态识别结果
  67. """
  68. # 计算技术指标
  69. volatility = self._calculate_volatility(price_data)
  70. volume_ma5 = price_data['volume'].rolling(5).mean().iloc[-1]
  71. volume_ma20 = price_data['volume'].rolling(20).mean().iloc[-1]
  72. volume_ratio = volume_ma5 / volume_ma20 if volume_ma20 > 0 else 1.0
  73. adx_value = self._calculate_adx(price_data)
  74. dispersion = self._calculate_dispersion(sector_data) if sector_data is not None else 50.0
  75. # 计算历史分位数
  76. vol_percentile = self._get_percentile(volatility, price_data)
  77. vol_low_30 = np.percentile(volatility.iloc[-60:], 30)
  78. vol_rebound = (volatility.iloc[-1] - vol_low_30) / vol_low_30 if vol_low_30 > 0 else 0
  79. # 四季识别逻辑
  80. regime_scores = {}
  81. # 春季:波动率从低位回升,成交量温和放大
  82. spring_score = self._score_spring(vol_rebound, volume_ratio, dispersion)
  83. regime_scores[MacroRegime.SPRING] = spring_score
  84. # 夏季:趋势明确,波动率稳定,情绪高涨
  85. summer_score = self._score_summer(adx_value, volatility, price_data, dispersion)
  86. regime_scores[MacroRegime.SUMMER] = summer_score
  87. # 秋季:板块离散度大,轮动加速
  88. autumn_score = self._score_autumn(dispersion, volatility, price_data)
  89. regime_scores[MacroRegime.AUTUMN] = autumn_score
  90. # 冬季:成交量萎缩,波动率压缩
  91. winter_score = self._score_winter(volume_ratio, vol_percentile, adx_value)
  92. regime_scores[MacroRegime.WINTER] = winter_score
  93. # 选择最高分的生态
  94. best_regime = max(regime_scores, key=regime_scores.get)
  95. best_score = regime_scores[best_regime]
  96. # 如果最高分低于阈值,标记为未知
  97. if best_score < 0.3:
  98. best_regime = MacroRegime.UNKNOWN
  99. best_score = 0.0
  100. return MacroEcosystem(
  101. regime=best_regime,
  102. confidence=best_score,
  103. volatility_trend=vol_rebound,
  104. volume_trend=volume_ratio - 1.0,
  105. dispersion=dispersion,
  106. adx_value=adx_value,
  107. description=self._get_description(best_regime)
  108. )
  109. def _calculate_volatility(self, data: pd.DataFrame, period: int = 20) -> pd.Series:
  110. """计算波动率(20日收益率标准差年化)"""
  111. returns = data['close'].pct_change()
  112. volatility = returns.rolling(period).std() * np.sqrt(252)
  113. return volatility
  114. def _calculate_adx(self, data: pd.DataFrame, period: int = 14) -> float:
  115. """计算ADX(平均趋向指数)"""
  116. high = data['high']
  117. low = data['low']
  118. close = data['close']
  119. # +DM和-DM
  120. plus_dm = high.diff()
  121. minus_dm = -low.diff()
  122. plus_dm[plus_dm < 0] = 0
  123. minus_dm[minus_dm < 0] = 0
  124. # TR(真实波幅)
  125. tr1 = high - low
  126. tr2 = abs(high - close.shift(1))
  127. tr3 = abs(low - close.shift(1))
  128. tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
  129. # ATR
  130. atr = tr.rolling(period).mean()
  131. # +DI和-DI
  132. plus_di = 100 * (plus_dm.rolling(period).mean() / atr)
  133. minus_di = 100 * (minus_dm.rolling(period).mean() / atr)
  134. # DX和ADX
  135. dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di)
  136. adx = dx.rolling(period).mean()
  137. return adx.iloc[-1] if not pd.isna(adx.iloc[-1]) else 20.0
  138. def _calculate_dispersion(self, sector_data: pd.DataFrame) -> float:
  139. """计算板块离散度(成分股收益率标准差的历史分位)"""
  140. if sector_data is None or sector_data.empty:
  141. return 50.0
  142. # 计算各成分股近期收益率
  143. returns = sector_data.pct_change().iloc[-20:]
  144. cross_sectional_std = returns.std(axis=1).mean()
  145. # 转换为历史分位数(简化处理)
  146. historical_std = returns.std(axis=1).rolling(60).mean()
  147. if len(historical_std) > 0 and not pd.isna(historical_std.iloc[-1]):
  148. percentile = 50 + (cross_sectional_std - historical_std.mean()) / historical_std.std() * 25
  149. return max(0, min(100, percentile))
  150. return 50.0
  151. def _get_percentile(self, series: pd.Series, data: pd.DataFrame) -> float:
  152. """获取当前值的历史分位数"""
  153. if len(series) < 60:
  154. return 50.0
  155. current = series.iloc[-1]
  156. historical = series.iloc[-252:-1].dropna()
  157. if len(historical) == 0:
  158. return 50.0
  159. return (historical < current).mean() * 100
  160. def _score_spring(self, vol_rebound: float, volume_ratio: float, dispersion: float) -> float:
  161. """评分:春季(复苏)"""
  162. # 波动率从低位回升
  163. vol_score = min(1.0, vol_rebound / self.spring_vol_rebound)
  164. # 成交量放大
  165. volume_score = min(1.0, max(0, (volume_ratio - 1.0) / (self.spring_volume_ratio - 1.0)))
  166. # 板块离散度中等(30-60分位)
  167. dispersion_score = 1.0 - abs(dispersion - 45) / 15 if 30 <= dispersion <= 60 else 0.0
  168. return vol_score * 0.4 + volume_score * 0.4 + dispersion_score * 0.2
  169. def _score_summer(self, adx: float, volatility: pd.Series, data: pd.DataFrame, dispersion: float) -> float:
  170. """评分:夏季(繁荣)"""
  171. # ADX高于阈值
  172. adx_score = min(1.0, adx / self.summer_adx_threshold) if adx > 20 else 0.0
  173. # 波动率稳定(历史40-70分位)
  174. vol_percentile = self._get_percentile(volatility, data)
  175. vol_score = 1.0 - abs(vol_percentile - 55) / 15 if 40 <= vol_percentile <= 70 else 0.0
  176. # 板块离散度低(同涨同跌)
  177. dispersion_score = 1.0 - dispersion / 40 if dispersion < 40 else 0.0
  178. return adx_score * 0.5 + vol_score * 0.3 + dispersion_score * 0.2
  179. def _score_autumn(self, dispersion: float, volatility: pd.Series, data: pd.DataFrame) -> float:
  180. """评分:秋季(分化)"""
  181. # 板块离散度高
  182. dispersion_score = min(1.0, (dispersion - 50) / 30) if dispersion > 50 else 0.0
  183. # 波动率上升
  184. vol_recent = volatility.iloc[-5:].mean()
  185. vol_prev = volatility.iloc[-20:-5].mean()
  186. vol_increase = (vol_recent - vol_prev) / vol_prev if vol_prev > 0 else 0
  187. vol_score = min(1.0, vol_increase / 0.15)
  188. return dispersion_score * 0.6 + vol_score * 0.4
  189. def _score_winter(self, volume_ratio: float, vol_percentile: float, adx: float) -> float:
  190. """评分:冬季(萧条)"""
  191. # 成交量萎缩
  192. volume_score = 1.0 - min(1.0, volume_ratio) if volume_ratio < 1.0 else 0.0
  193. # 波动率压缩至历史低位
  194. vol_score = 1.0 - vol_percentile / self.winter_vol_compression if vol_percentile < self.winter_vol_compression else 0.0
  195. # 无明确趋势
  196. adx_score = 1.0 - min(1.0, adx / 20) if adx < 25 else 0.0
  197. return volume_score * 0.3 + vol_score * 0.4 + adx_score * 0.3
  198. def _get_description(self, regime: MacroRegime) -> str:
  199. """获取生态描述"""
  200. descriptions = {
  201. MacroRegime.SPRING: "春季复苏:波动率回升,成交温和放大,机构左侧布局",
  202. MacroRegime.SUMMER: "夏季繁荣:趋势明确,情绪高涨,散户机构共振",
  203. MacroRegime.AUTUMN: "秋季分化:板块离散,轮动加速,量化游资主导",
  204. MacroRegime.WINTER: "冬季萧条:成交萎缩,波动压缩,空头主导",
  205. MacroRegime.UNKNOWN: "过渡状态:生态不明,建议观望"
  206. }
  207. return descriptions.get(regime, "未知状态")