stock-analyzer-skill 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +21 -0
  3. package/CHANGELOG.md +93 -0
  4. package/CONTRIBUTING.md +331 -0
  5. package/README.md +259 -0
  6. package/experts/README.md +119 -0
  7. package/experts/buffett.md +91 -0
  8. package/experts/chaogu_yangjia.md +125 -0
  9. package/experts/decide.md +212 -0
  10. package/experts/duan_yongping.md +106 -0
  11. package/experts/lynch.md +127 -0
  12. package/experts/soros.md +89 -0
  13. package/experts/xu_xiang.md +107 -0
  14. package/experts/zhao_laoge.md +143 -0
  15. package/experts/zuoshou_xinyi.md +144 -0
  16. package/install-plugin.js +69 -0
  17. package/methodology.md +455 -0
  18. package/package.json +43 -0
  19. package/scripts/__pycache__/announcements.cpython-314.pyc +0 -0
  20. package/scripts/__pycache__/backtest.cpython-314.pyc +0 -0
  21. package/scripts/__pycache__/chan.cpython-314.pyc +0 -0
  22. package/scripts/__pycache__/classifier.cpython-314.pyc +0 -0
  23. package/scripts/__pycache__/common.cpython-314.pyc +0 -0
  24. package/scripts/__pycache__/finance.cpython-314.pyc +0 -0
  25. package/scripts/__pycache__/init_pool.cpython-314.pyc +0 -0
  26. package/scripts/__pycache__/kline.cpython-314.pyc +0 -0
  27. package/scripts/__pycache__/patterns_local.cpython-314.pyc +0 -0
  28. package/scripts/__pycache__/quote.cpython-314.pyc +0 -0
  29. package/scripts/__pycache__/refresh_pool.cpython-314.pyc +0 -0
  30. package/scripts/__pycache__/screener.cpython-314.pyc +0 -0
  31. package/scripts/__pycache__/technical.cpython-314.pyc +0 -0
  32. package/scripts/announcements.py +118 -0
  33. package/scripts/backtest.py +528 -0
  34. package/scripts/chan.py +591 -0
  35. package/scripts/classifier.py +302 -0
  36. package/scripts/common.py +507 -0
  37. package/scripts/data/__init__.py +208 -0
  38. package/scripts/data/__pycache__/__init__.cpython-314.pyc +0 -0
  39. package/scripts/data/__pycache__/cache.cpython-314.pyc +0 -0
  40. package/scripts/data/__pycache__/config.cpython-314.pyc +0 -0
  41. package/scripts/data/__pycache__/types.cpython-314.pyc +0 -0
  42. package/scripts/data/cache.py +99 -0
  43. package/scripts/data/config.py +49 -0
  44. package/scripts/data/industry_thresholds.json +199 -0
  45. package/scripts/data/portfolio_example.json +14 -0
  46. package/scripts/data/sector_etf.csv +14 -0
  47. package/scripts/data/sector_mapping.json +64 -0
  48. package/scripts/data/sector_stocks.json +135 -0
  49. package/scripts/data/types.py +66 -0
  50. package/scripts/fetchers/__init__.py +130 -0
  51. package/scripts/fetchers/__pycache__/__init__.cpython-314.pyc +0 -0
  52. package/scripts/fetchers/__pycache__/akshare_finance.cpython-314.pyc +0 -0
  53. package/scripts/fetchers/__pycache__/akshare_kline.cpython-314.pyc +0 -0
  54. package/scripts/fetchers/__pycache__/akshare_quote.cpython-314.pyc +0 -0
  55. package/scripts/fetchers/__pycache__/baostock_kline.cpython-314.pyc +0 -0
  56. package/scripts/fetchers/__pycache__/eastmoney_finance.cpython-314.pyc +0 -0
  57. package/scripts/fetchers/__pycache__/eastmoney_kline.cpython-314.pyc +0 -0
  58. package/scripts/fetchers/__pycache__/eastmoney_quote.cpython-314.pyc +0 -0
  59. package/scripts/fetchers/__pycache__/efinance_finance.cpython-314.pyc +0 -0
  60. package/scripts/fetchers/__pycache__/efinance_kline.cpython-314.pyc +0 -0
  61. package/scripts/fetchers/__pycache__/efinance_quote.cpython-314.pyc +0 -0
  62. package/scripts/fetchers/__pycache__/pytdx_quote.cpython-314.pyc +0 -0
  63. package/scripts/fetchers/__pycache__/sina_kline.cpython-314.pyc +0 -0
  64. package/scripts/fetchers/__pycache__/sina_quote.cpython-314.pyc +0 -0
  65. package/scripts/fetchers/__pycache__/tencent_kline.cpython-314.pyc +0 -0
  66. package/scripts/fetchers/__pycache__/tencent_quote.cpython-314.pyc +0 -0
  67. package/scripts/fetchers/__pycache__/tushare_kline.cpython-314.pyc +0 -0
  68. package/scripts/fetchers/__pycache__/tushare_quote.cpython-314.pyc +0 -0
  69. package/scripts/fetchers/__pycache__/yfinance_kline.cpython-314.pyc +0 -0
  70. package/scripts/fetchers/akshare_finance.py +35 -0
  71. package/scripts/fetchers/akshare_kline.py +59 -0
  72. package/scripts/fetchers/akshare_quote.py +52 -0
  73. package/scripts/fetchers/baostock_kline.py +64 -0
  74. package/scripts/fetchers/eastmoney_finance.py +29 -0
  75. package/scripts/fetchers/eastmoney_kline.py +48 -0
  76. package/scripts/fetchers/eastmoney_quote.py +68 -0
  77. package/scripts/fetchers/efinance_finance.py +32 -0
  78. package/scripts/fetchers/efinance_kline.py +46 -0
  79. package/scripts/fetchers/efinance_quote.py +53 -0
  80. package/scripts/fetchers/pytdx_kline.py +70 -0
  81. package/scripts/fetchers/pytdx_quote.py +78 -0
  82. package/scripts/fetchers/sina_kline.py +30 -0
  83. package/scripts/fetchers/sina_quote.py +35 -0
  84. package/scripts/fetchers/tencent_kline.py +52 -0
  85. package/scripts/fetchers/tencent_quote.py +29 -0
  86. package/scripts/fetchers/tushare_kline.py +62 -0
  87. package/scripts/fetchers/tushare_quote.py +62 -0
  88. package/scripts/fetchers/yfinance_kline.py +66 -0
  89. package/scripts/finance.py +92 -0
  90. package/scripts/init_pool.py +105 -0
  91. package/scripts/kline.py +62 -0
  92. package/scripts/monitor.py +107 -0
  93. package/scripts/patterns_local.py +599 -0
  94. package/scripts/quote.py +69 -0
  95. package/scripts/refresh_pool.py +328 -0
  96. package/scripts/screener.py +434 -0
  97. package/scripts/strategies/__init__.py +11 -0
  98. package/scripts/strategies/__pycache__/__init__.cpython-314.pyc +0 -0
  99. package/scripts/strategies/__pycache__/registry.cpython-314.pyc +0 -0
  100. package/scripts/strategies/__pycache__/thresholds.cpython-314.pyc +0 -0
  101. package/scripts/strategies/factors/__init__.py +8 -0
  102. package/scripts/strategies/factors/__pycache__/__init__.cpython-314.pyc +0 -0
  103. package/scripts/strategies/factors/__pycache__/liquidity.cpython-314.pyc +0 -0
  104. package/scripts/strategies/factors/__pycache__/momentum.cpython-314.pyc +0 -0
  105. package/scripts/strategies/factors/__pycache__/quality.cpython-314.pyc +0 -0
  106. package/scripts/strategies/factors/__pycache__/valuation.cpython-314.pyc +0 -0
  107. package/scripts/strategies/factors/__pycache__/volatility.cpython-314.pyc +0 -0
  108. package/scripts/strategies/factors/liquidity.py +49 -0
  109. package/scripts/strategies/factors/momentum.py +45 -0
  110. package/scripts/strategies/factors/quality.py +54 -0
  111. package/scripts/strategies/factors/valuation.py +76 -0
  112. package/scripts/strategies/factors/volatility.py +89 -0
  113. package/scripts/strategies/registry.py +87 -0
  114. package/scripts/strategies/thresholds.py +28 -0
  115. package/scripts/technical/__init__.py +116 -0
  116. package/scripts/technical/__pycache__/__init__.cpython-314.pyc +0 -0
  117. package/scripts/technical/__pycache__/astock.cpython-314.pyc +0 -0
  118. package/scripts/technical/__pycache__/boll.cpython-314.pyc +0 -0
  119. package/scripts/technical/__pycache__/candlestick.cpython-314.pyc +0 -0
  120. package/scripts/technical/__pycache__/core.cpython-314.pyc +0 -0
  121. package/scripts/technical/__pycache__/kdj.cpython-314.pyc +0 -0
  122. package/scripts/technical/__pycache__/macd.cpython-314.pyc +0 -0
  123. package/scripts/technical/__pycache__/moving_average.cpython-314.pyc +0 -0
  124. package/scripts/technical/__pycache__/report.cpython-314.pyc +0 -0
  125. package/scripts/technical/__pycache__/rsi.cpython-314.pyc +0 -0
  126. package/scripts/technical/__pycache__/scoring.cpython-314.pyc +0 -0
  127. package/scripts/technical/__pycache__/signals.cpython-314.pyc +0 -0
  128. package/scripts/technical/__pycache__/trend.cpython-314.pyc +0 -0
  129. package/scripts/technical/__pycache__/volume.cpython-314.pyc +0 -0
  130. package/scripts/technical/astock.py +98 -0
  131. package/scripts/technical/boll.py +49 -0
  132. package/scripts/technical/candlestick.py +151 -0
  133. package/scripts/technical/core.py +92 -0
  134. package/scripts/technical/kdj.py +68 -0
  135. package/scripts/technical/macd.py +97 -0
  136. package/scripts/technical/moving_average.py +59 -0
  137. package/scripts/technical/report.py +221 -0
  138. package/scripts/technical/rsi.py +37 -0
  139. package/scripts/technical/scoring.py +392 -0
  140. package/scripts/technical/signals.py +70 -0
  141. package/scripts/technical/trend.py +143 -0
  142. package/scripts/technical/volume.py +113 -0
  143. package/scripts/technical.py +215 -0
  144. package/skills/financial-analyst/SKILL.md +141 -0
  145. package/skills/help/SKILL.md +188 -0
  146. package/skills/init/SKILL.md +66 -0
  147. package/skills/investment-researcher/SKILL.md +152 -0
  148. package/skills/market/SKILL.md +99 -0
  149. package/skills/portfolio/SKILL.md +96 -0
  150. package/skills/screener/SKILL.md +128 -0
  151. package/skills/sector/SKILL.md +102 -0
  152. package/skills/stock/SKILL.md +148 -0
  153. package/skills/technical/SKILL.md +168 -0
  154. package/workflow.md +91 -0
@@ -0,0 +1,76 @@
1
+ """
2
+ 估值因子评分:PE、PB、PEG、PS(市销率)。
3
+ PS 对亏损但有收入的公司是唯一可用的估值指标。
4
+ """
5
+ from common import to_float, clamp
6
+ from strategies.thresholds import get_industry_threshold
7
+
8
+
9
+ def valuation_score(quote: dict, fin: dict, industry: str = "默认") -> float:
10
+ """估值因子评分(行业差异化)。满分 100。"""
11
+ pe = to_float(quote.get("pe"))
12
+ pb = to_float(quote.get("pb"))
13
+ growth = max(to_float(fin.get("net_profit_yoy", fin.get("PARENTNETPROFITTZ"))), 0)
14
+
15
+ # 计算 PS(市销率):总市值 / 营收
16
+ total_cap = to_float(quote.get("total_cap")) # 亿
17
+ revenue_yoy = to_float(fin.get("revenue_yoy", fin.get("TOTALOPERATEREVETZ")))
18
+ # 如果有营收同比和市值,可以估算 PS 区间
19
+ # 简化:直接用行业 PS 阈值评估
20
+
21
+ # 行业差异化 PE 阈值
22
+ pe_undervalued = get_industry_threshold(industry, "pe_undervalued", 15)
23
+ pe_reasonable = get_industry_threshold(industry, "pe_reasonable", 25)
24
+ pe_expensive = get_industry_threshold(industry, "pe_expensive", 40)
25
+ peg_undervalued = get_industry_threshold(industry, "peg_undervalued", 0.8)
26
+ peg_reasonable = get_industry_threshold(industry, "peg_reasonable", 1.5)
27
+
28
+ # PE 极端值截断:超过行业 expensive 阈值 2 倍时,PE 评分为 0
29
+ pe_cap = pe_expensive * 2
30
+
31
+ score = 0
32
+ # PE 评分(行业差异化)
33
+ if pe <= 0:
34
+ # 亏损股:PB 仍可参考(净资产为正时)
35
+ if 0 < pb <= 1:
36
+ score += 20 # 破净,可能被低估
37
+ elif 1 < pb <= 2:
38
+ score += 12
39
+ # 亏损收窄加分
40
+ if growth > 0:
41
+ score += 10
42
+ # PS 评分(亏损但有收入的公司)
43
+ if total_cap > 0 and revenue_yoy > 0:
44
+ # 简化 PS:用营收增速作为 proxy
45
+ # 高增速亏损公司(如互联网早期)仍可给分
46
+ if revenue_yoy > 30:
47
+ score += 15 # 高增长亏损,PS 视角可接受
48
+ elif revenue_yoy > 10:
49
+ score += 8
50
+ elif pe > pe_cap:
51
+ # PE 超过极端阈值,PE 评分为 0(但 PB 和 PEG 仍可评分)
52
+ pass
53
+ elif 0 < pe <= pe_undervalued:
54
+ score += 38
55
+ elif pe_undervalued < pe <= pe_reasonable:
56
+ score += 38 - (pe - pe_undervalued) / (pe_reasonable - pe_undervalued) * 18
57
+ elif pe_reasonable < pe <= pe_expensive:
58
+ score += 20 - (pe - pe_reasonable) / (pe_expensive - pe_reasonable) * 10
59
+
60
+ # PB 评分
61
+ if 0 < pb <= 2:
62
+ score += 24
63
+ elif 2 < pb <= 5:
64
+ score += 24 - (pb - 2) / 3 * 14
65
+
66
+ # PEG 评分(仅盈利股,且 PE 未超极端阈值)
67
+ if 0 < pe <= pe_cap and growth > 0:
68
+ peg = pe / growth
69
+ if peg <= peg_undervalued:
70
+ score += 28
71
+ elif peg <= peg_reasonable:
72
+ score += 22
73
+ elif peg <= peg_reasonable * 1.5:
74
+ score += 12
75
+
76
+ return clamp(score)
@@ -0,0 +1,89 @@
1
+ """
2
+ 波动率因子评分:基于历史收益率标准差,低波动得高分。
3
+ A 股低波动异象显著——低波动股票长期跑赢高波动股票。
4
+ """
5
+ from common import clamp
6
+ from strategies.thresholds import get_industry_threshold
7
+
8
+
9
+ def _stdev(values):
10
+ """计算标准差(纯 Python 实现,无需 statistics 模块)。"""
11
+ n = len(values)
12
+ if n < 2:
13
+ return 0.0
14
+ mean = sum(values) / n
15
+ variance = sum((x - mean) ** 2 for x in values) / (n - 1)
16
+ return variance ** 0.5
17
+
18
+
19
+ def _vol_score(vol, vol_threshold):
20
+ """根据波动率和阈值计算评分(低波动得高分)。"""
21
+ if vol <= vol_threshold * 0.4:
22
+ return 95 # 极低波动
23
+ elif vol <= vol_threshold * 0.7:
24
+ return 80 # 低波动
25
+ elif vol <= vol_threshold:
26
+ return 65 # 正常波动(偏稳)
27
+ elif vol <= vol_threshold * 1.3:
28
+ return 50 # 正常波动
29
+ elif vol <= vol_threshold * 1.8:
30
+ return 30 # 高波动
31
+ elif vol <= vol_threshold * 2.5:
32
+ return 15 # 很高波动
33
+ else:
34
+ return 5 # 极高波动
35
+
36
+
37
+ def volatility_score(kline_bars: list, industry: str = "默认") -> float:
38
+ """波动率因子评分(行业差异化)。满分 100,低波动得高分。
39
+
40
+ Args:
41
+ kline_bars: K 线数据列表(至少 20 根),每根需有 close 属性
42
+ industry: 行业名称,用于加载差异化阈值
43
+
44
+ Returns:
45
+ 0-100 的评分,波动率越低得分越高
46
+ """
47
+ if len(kline_bars) < 20:
48
+ return 50 # 数据不足,返回中性分
49
+
50
+ # 计算 20 日日收益率标准差
51
+ recent = kline_bars[-20:]
52
+ returns = []
53
+ for i in range(1, len(recent)):
54
+ if recent[i - 1].close > 0:
55
+ returns.append((recent[i].close - recent[i - 1].close) / recent[i - 1].close)
56
+
57
+ if len(returns) < 10:
58
+ return 50
59
+
60
+ vol = _stdev(returns)
61
+ vol_threshold = get_industry_threshold(industry, "vol_threshold", 0.025)
62
+ return _vol_score(vol, vol_threshold)
63
+
64
+
65
+ def volatility_from_closes(closes: list, industry: str = "默认") -> float:
66
+ """从收盘价列表计算波动率评分(兼容 screener 的 dict 数据)。
67
+
68
+ Args:
69
+ closes: 收盘价列表(至少 20 个)
70
+ industry: 行业名称
71
+
72
+ Returns:
73
+ 0-100 的评分
74
+ """
75
+ if len(closes) < 20:
76
+ return 50
77
+
78
+ recent = closes[-20:]
79
+ returns = []
80
+ for i in range(1, len(recent)):
81
+ if recent[i - 1] > 0:
82
+ returns.append((recent[i] - recent[i - 1]) / recent[i - 1])
83
+
84
+ if len(returns) < 10:
85
+ return 50
86
+
87
+ vol = _stdev(returns)
88
+ vol_threshold = get_industry_threshold(industry, "vol_threshold", 0.025)
89
+ return _vol_score(vol, vol_threshold)
@@ -0,0 +1,87 @@
1
+ """
2
+ 策略注册表:管理策略定义和权重配置。
3
+ """
4
+ from typing import Dict, Optional
5
+
6
+ # ---------- 内置策略定义 ----------
7
+ # 五因子:quality / valuation / momentum / liquidity / volatility
8
+ # volatility 为 A 股低波动异象因子,低波动得高分
9
+
10
+ STRATEGIES: Dict[str, dict] = {
11
+ "balanced": {
12
+ "quality": 0.25,
13
+ "valuation": 0.20,
14
+ "momentum": 0.20,
15
+ "liquidity": 0.15,
16
+ "volatility": 0.20,
17
+ "label": "均衡精选",
18
+ },
19
+ "quality_value": {
20
+ "quality": 0.35,
21
+ "valuation": 0.30,
22
+ "momentum": 0.05,
23
+ "liquidity": 0.12,
24
+ "volatility": 0.18,
25
+ "label": "质量价值",
26
+ },
27
+ "growth_momentum": {
28
+ "quality": 0.18,
29
+ "valuation": 0.15,
30
+ "momentum": 0.35,
31
+ "liquidity": 0.12,
32
+ "volatility": 0.20,
33
+ "label": "成长动量",
34
+ },
35
+ "defensive": {
36
+ "quality": 0.25,
37
+ "valuation": 0.22,
38
+ "momentum": 0.08,
39
+ "liquidity": 0.12,
40
+ "volatility": 0.33,
41
+ "label": "防守低波",
42
+ },
43
+ "turning_point": {
44
+ "quality": 0.18,
45
+ "valuation": 0.18,
46
+ "momentum": 0.32,
47
+ "liquidity": 0.14,
48
+ "volatility": 0.18,
49
+ "label": "拐点修复",
50
+ },
51
+ }
52
+
53
+
54
+ # ---------- 策略注册 API ----------
55
+
56
+ def register_strategy(name: str, weights: dict, label: str = "") -> None:
57
+ """注册新策略。
58
+
59
+ Args:
60
+ name: 策略名称
61
+ weights: 因子权重 dict,需包含 quality/valuation/momentum/liquidity
62
+ volatility 为可选因子(默认 0)
63
+ label: 策略中文标签
64
+ """
65
+ required_keys = {"quality", "valuation", "momentum", "liquidity"}
66
+ if not required_keys.issubset(weights.keys()):
67
+ raise ValueError(f"策略权重必须包含 {required_keys}")
68
+ # volatility 为可选,默认 0
69
+ if "volatility" not in weights:
70
+ weights["volatility"] = 0.0
71
+ all_keys = required_keys | {"volatility"}
72
+ total = sum(weights.get(k, 0) for k in all_keys)
73
+ if abs(total - 1.0) > 0.01:
74
+ raise ValueError(f"权重之和应为 1.0,当前为 {total}")
75
+ STRATEGIES[name] = {**weights, "label": label or name}
76
+
77
+
78
+ def get_strategy(name: str) -> dict:
79
+ """获取策略配置。"""
80
+ if name not in STRATEGIES:
81
+ raise KeyError(f"未知策略: {name},可用: {list(STRATEGIES.keys())}")
82
+ return STRATEGIES[name]
83
+
84
+
85
+ def list_strategies() -> list:
86
+ """列出所有策略名称。"""
87
+ return list(STRATEGIES.keys())
@@ -0,0 +1,28 @@
1
+ """
2
+ 行业差异化阈值管理。
3
+ 从 data/industry_thresholds.json 加载,供因子评分使用。
4
+ """
5
+ import json
6
+ from pathlib import Path
7
+
8
+ _industry_thresholds = None
9
+
10
+
11
+ def load_industry_thresholds() -> dict:
12
+ """加载行业差异化阈值表。"""
13
+ global _industry_thresholds
14
+ if _industry_thresholds is None:
15
+ from common import DATA_DIR
16
+ path = DATA_DIR / "industry_thresholds.json"
17
+ if path.exists():
18
+ _industry_thresholds = json.loads(path.read_text(encoding="utf-8"))
19
+ else:
20
+ _industry_thresholds = {}
21
+ return _industry_thresholds
22
+
23
+
24
+ def get_industry_threshold(industry: str, key: str, default=None):
25
+ """获取行业特定阈值。"""
26
+ thresholds = load_industry_thresholds()
27
+ industry_cfg = thresholds.get(industry, thresholds.get("默认", {}))
28
+ return industry_cfg.get(key, default)
@@ -0,0 +1,116 @@
1
+ """
2
+ technical 包:A 股纯技术分析模块化包。
3
+
4
+ 公开函数保持与原 technical.py 完全兼容。
5
+ """
6
+ # 数学工具与数据解析
7
+ from .core import (
8
+ sma,
9
+ ema,
10
+ _ema_series,
11
+ stddev,
12
+ _find_swing_points,
13
+ _parse_records,
14
+ )
15
+
16
+ # 均线系统
17
+ from .moving_average import (
18
+ ma_system,
19
+ _MA_PERIODS,
20
+ )
21
+
22
+ # MACD
23
+ from .macd import (
24
+ macd_full,
25
+ _detect_macd_divergence,
26
+ )
27
+
28
+ # KDJ
29
+ from .kdj import kdj_full
30
+
31
+ # 布林带
32
+ from .boll import bollinger
33
+
34
+ # RSI
35
+ from .rsi import rsi_features
36
+
37
+ # 成交量
38
+ from .volume import (
39
+ volume_analysis,
40
+ _obv_series,
41
+ _detect_obv_divergence,
42
+ )
43
+
44
+ # K 线形态
45
+ from .candlestick import (
46
+ detect_candle_patterns,
47
+ _body_shadow,
48
+ _is_bullish,
49
+ _candle_single,
50
+ _candle_double,
51
+ _candle_triple,
52
+ _candle_ashare,
53
+ )
54
+
55
+ # 趋势结构
56
+ from .trend import (
57
+ support_resistance,
58
+ box_detection,
59
+ breakout_check,
60
+ wave_state,
61
+ )
62
+
63
+ # A 股特化
64
+ from .astock import (
65
+ limit_analysis,
66
+ _count_limit_streak,
67
+ )
68
+
69
+ # 综合评分
70
+ from .scoring import (
71
+ composite_score,
72
+ detect_market_environment,
73
+ _market_weight_adjustments,
74
+ _STOCK_TYPE_WEIGHTS,
75
+ )
76
+
77
+ # 买卖信号
78
+ from .signals import _generate_signals
79
+
80
+ # 报告渲染
81
+ from .report import (
82
+ _fmt,
83
+ render_report,
84
+ render_quick,
85
+ )
86
+
87
+ __all__ = [
88
+ # core
89
+ "sma", "ema", "_ema_series", "stddev", "_find_swing_points", "_parse_records",
90
+ # moving_average
91
+ "ma_system", "_MA_PERIODS",
92
+ # macd
93
+ "macd_full", "_detect_macd_divergence",
94
+ # kdj
95
+ "kdj_full",
96
+ # boll
97
+ "bollinger",
98
+ # rsi
99
+ "rsi_features",
100
+ # volume
101
+ "volume_analysis", "_obv_series", "_detect_obv_divergence",
102
+ # candlestick
103
+ "detect_candle_patterns", "_body_shadow", "_is_bullish",
104
+ "_candle_single", "_candle_double", "_candle_triple", "_candle_ashare",
105
+ # trend
106
+ "support_resistance", "box_detection", "breakout_check", "wave_state",
107
+ # astock
108
+ "limit_analysis", "_count_limit_streak",
109
+ # scoring
110
+ "composite_score", "detect_market_environment",
111
+ "_market_weight_adjustments", "_STOCK_TYPE_WEIGHTS",
112
+ # signals
113
+ "_generate_signals",
114
+ # report
115
+ "_fmt", "render_report", "render_quick",
116
+ ]
@@ -0,0 +1,98 @@
1
+ """
2
+ A 股特化分析(涨跌停、连板)。
3
+ 依赖: common (to_float)
4
+ """
5
+ from common import to_float
6
+
7
+
8
+ _LIMIT_RATIOS = {"主板": 9.5, "创业板": 19.5, "科创板": 19.5, "北交所": 29.5}
9
+
10
+
11
+ def limit_analysis(records, board, quote):
12
+ """涨跌停/连板分析。"""
13
+ if len(records) < 10:
14
+ return None
15
+
16
+ limit_ratio = _LIMIT_RATIOS.get(board, 9.5)
17
+ limit_up_price = to_float(quote.get("limit_up"))
18
+ limit_down_price = to_float(quote.get("limit_down"))
19
+ last_close = to_float(records[-1].get("close"))
20
+ last_high = to_float(records[-1].get("high"))
21
+ last_low = to_float(records[-1].get("low"))
22
+ last_open = to_float(records[-1].get("open"))
23
+
24
+ result = {
25
+ "board": board,
26
+ "limit_ratio": limit_ratio,
27
+ "limit_up_price": limit_up_price,
28
+ "limit_down_price": limit_down_price,
29
+ }
30
+
31
+ # 当前涨跌停状态
32
+ if limit_up_price > 0 and last_close >= limit_up_price * 0.995:
33
+ result["board_status"] = "封涨停"
34
+ elif last_low <= limit_down_price * 1.005 if limit_down_price > 0 else False:
35
+ if last_close > limit_down_price * 1.01:
36
+ result["board_status"] = "翘板(跌停打开)"
37
+ else:
38
+ result["board_status"] = "封跌停"
39
+ elif last_high >= limit_up_price * 0.995 and last_close < limit_up_price * 0.995:
40
+ gap = (limit_up_price - last_close) / limit_up_price * 100
41
+ result["board_status"] = f"炸板(离涨停差{gap:.1f}%)"
42
+ else:
43
+ result["board_status"] = "正常交易"
44
+
45
+ # 连板检测
46
+ streak = _count_limit_streak(records, limit_ratio)
47
+ result["limit_streak"] = streak
48
+
49
+ if streak > 0:
50
+ if streak == 1:
51
+ result["streak_type"] = "首板"
52
+ elif streak == 2:
53
+ result["streak_type"] = "二板(连板确认)"
54
+ elif streak <= 4:
55
+ result["streak_type"] = f"高位{streak}板"
56
+ else:
57
+ result["streak_type"] = f"妖股({streak}连板)"
58
+
59
+ # 连板量能分析
60
+ recent_bars = records[-streak:]
61
+ vols = [to_float(r.get("volume")) for r in recent_bars]
62
+ if len(vols) >= 2 and vols[0] > 0:
63
+ if all(vols[i] < vols[i-1] for i in range(1, len(vols))):
64
+ result["streak_volume"] = "缩量加速(强-惜售)"
65
+ elif vols[-1] > vols[0] * 1.5:
66
+ result["streak_volume"] = "放量分歧(弱-换手加大)"
67
+ else:
68
+ result["streak_volume"] = "量能稳定(中性)"
69
+ else:
70
+ result["streak_type"] = "无连板"
71
+
72
+ # T+1 风险提示
73
+ if streak >= 1 and result.get("board_status") == "封涨停":
74
+ result["t1_risk"] = "T+1隔夜风险:今日追板仓位明日方可卖出,需关注次日溢价和核按钮风险"
75
+ else:
76
+ result["t1_risk"] = None
77
+
78
+ return result
79
+
80
+
81
+ def _count_limit_streak(records, limit_ratio):
82
+ """计算当前连板数。"""
83
+ count = 0
84
+ for i in range(len(records) - 1, -1, -1):
85
+ r = records[i]
86
+ close = to_float(r.get("close"))
87
+ prev_close = to_float(records[i - 1].get("close")) if i > 0 else close
88
+ if prev_close <= 0:
89
+ continue
90
+ chg_pct = (close / prev_close - 1) * 100
91
+ if i == len(records) - 1:
92
+ if chg_pct >= limit_ratio * 0.95:
93
+ count = 1
94
+ elif count > 0 and chg_pct >= limit_ratio * 0.95:
95
+ count += 1
96
+ else:
97
+ break
98
+ return count
@@ -0,0 +1,49 @@
1
+ """
2
+ 布林带分析。
3
+ 依赖: core (sma, stddev)
4
+ """
5
+ from .core import sma, stddev
6
+
7
+
8
+ def bollinger(closes, period=20, multiplier=2.0):
9
+ """布林带分析:上/中/下轨 + 带宽 + 价格位置。"""
10
+ if len(closes) < period:
11
+ return None
12
+
13
+ mid = sma(closes, period)
14
+ sd = stddev(closes[-period:])
15
+ upper = mid + multiplier * sd
16
+ lower = mid - multiplier * sd
17
+ bandwidth = (upper - lower) / mid if mid > 0 else 0
18
+ last = closes[-1]
19
+ position = (last - lower) / (upper - lower) if upper != lower else 0.5
20
+
21
+ # 带宽状态
22
+ if bandwidth < 0.05:
23
+ bw_desc = "极度收窄(变盘信号)"
24
+ elif bandwidth < 0.10:
25
+ bw_desc = "收窄中"
26
+ else:
27
+ bw_desc = "正常带宽"
28
+
29
+ # 价格位置
30
+ if position > 0.9:
31
+ pos_desc = "触及上轨"
32
+ elif position < 0.1:
33
+ pos_desc = "触及下轨"
34
+ elif position > 0.7:
35
+ pos_desc = "偏上轨"
36
+ elif position < 0.3:
37
+ pos_desc = "偏下轨"
38
+ else:
39
+ pos_desc = "中轨附近"
40
+
41
+ return {
42
+ "upper": round(upper, 2),
43
+ "mid": round(mid, 2),
44
+ "lower": round(lower, 2),
45
+ "bandwidth": round(bandwidth, 4),
46
+ "bandwidth_desc": bw_desc,
47
+ "position": round(position, 3),
48
+ "position_desc": pos_desc,
49
+ }