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,221 @@
1
+ """
2
+ 报告渲染。
3
+ 无内部依赖。
4
+ """
5
+
6
+
7
+ def _fmt(val, default="-"):
8
+ return str(val) if val is not None else default
9
+
10
+
11
+ def render_report(features, score, signals, meta):
12
+ """完整技术分析报告。"""
13
+ lines = []
14
+ ma = features.get("ma_system", {})
15
+ macd = features.get("macd") or {}
16
+ kdj = features.get("kdj") or {}
17
+ boll = features.get("bollinger") or {}
18
+ rsi_data = features.get("rsi", {})
19
+ vol = features.get("volume") or {}
20
+ sr = features.get("support_resistance", {})
21
+ box = features.get("box")
22
+ breakout = features.get("breakout", {})
23
+ wave = features.get("wave", "")
24
+ patterns = features.get("patterns", [])
25
+ limit_data = features.get("limit_analysis") or {}
26
+
27
+ lines.append("═" * 72)
28
+ lines.append(f" {meta['name']} ({meta['code']}) 现价: {meta['price']} 涨跌: {meta['change_pct']}% 板块: {meta['board']} 时间: {meta['timestamp']}")
29
+ lines.append("═" * 72)
30
+
31
+ # ── 综合评分 ──
32
+ lines.append(f"\n## 综合评分: {score['score']}/100 -- {score['grade']}")
33
+ if score["buy_signals"]:
34
+ lines.append(f" 买入信号: {', '.join(score['buy_signals'])}")
35
+ if score["sell_signals"]:
36
+ lines.append(f" 卖出信号: {', '.join(score['sell_signals'])}")
37
+ if sr.get("nearest_support"):
38
+ lines.append(f" 关键支撑: {sr['nearest_support']} 关键阻力: {sr.get('nearest_resistance', '-')}")
39
+
40
+ # ── 均线系统 ──
41
+ lines.append(f"\n## 均线系统")
42
+ ma_parts = []
43
+ for p in [5, 10, 20, 60, 120, 250]:
44
+ v = ma.get(f"ma{p}")
45
+ if v is not None:
46
+ ma_parts.append(f"MA{p}: {v}")
47
+ lines.append(f" {', '.join(ma_parts)}")
48
+ lines.append(f" 排列: {ma.get('alignment', '-')} | 粘合度: {ma.get('convergence_desc', '-')}")
49
+
50
+ # ── MACD ──
51
+ lines.append(f"\n## MACD")
52
+ lines.append(f" DIF: {_fmt(macd.get('dif'))} DEA: {_fmt(macd.get('dea'))} BAR: {_fmt(macd.get('macd_bar'))}")
53
+ lines.append(f" 信号: {macd.get('signal_desc', '-')} | 柱趋势: {macd.get('bar_trend', '-')}")
54
+ if macd.get("divergence"):
55
+ lines.append(f" 背离: **{macd['divergence']}**")
56
+
57
+ # ── KDJ ──
58
+ lines.append(f"\n## KDJ")
59
+ lines.append(f" K: {_fmt(kdj.get('k'))} D: {_fmt(kdj.get('d'))} J: {_fmt(kdj.get('j'))}")
60
+ lines.append(f" 信号: {kdj.get('signal', '-')}")
61
+ if kdj.get("钝化"):
62
+ lines.append(f" ⚠ KDJ钝化中,超买超卖信号暂停参考")
63
+
64
+ # ── BOLL ──
65
+ lines.append(f"\n## BOLL")
66
+ lines.append(f" 上轨: {_fmt(boll.get('upper'))} 中轨: {_fmt(boll.get('mid'))} 下轨: {_fmt(boll.get('lower'))}")
67
+ lines.append(f" 带宽: {boll.get('bandwidth_desc', '-')} | 价格: {boll.get('position_desc', '-')}")
68
+
69
+ # ── 成交量 ──
70
+ lines.append(f"\n## 成交量")
71
+ lines.append(f" 量比: {_fmt(vol.get('volume_ratio'))} ({vol.get('volume_ratio_desc', '-')})")
72
+ lines.append(f" 量价: {vol.get('volume_price', '-')}")
73
+ if vol.get("obv_divergence"):
74
+ lines.append(f" OBV: {vol['obv_divergence']}")
75
+
76
+ # ── RSI ──
77
+ lines.append(f"\n## RSI")
78
+ rsi_desc = {1: "超卖", -1: "超买"}.get(rsi_data.get("signal", 0), "正常")
79
+ lines.append(f" RSI-{rsi_data.get('period', 14)}: {rsi_data.get('rsi', 50)} ({rsi_desc})")
80
+
81
+ # ── K线形态 ──
82
+ if patterns:
83
+ lines.append(f"\n## K线形态")
84
+ for p in patterns:
85
+ lines.append(f" {p['position']} [{p['date']}] {p['type']}")
86
+ else:
87
+ lines.append(f"\n## K线形态\n (无明显形态)")
88
+
89
+ # ── 个股分类 ──
90
+ classification = features.get("classification")
91
+ if classification:
92
+ lines.append(f"\n## 个股分类")
93
+ lines.append(f" 类型: {classification['type']} (置信度: {classification['confidence']})")
94
+ if classification.get("reasons"):
95
+ lines.append(f" 依据: {'; '.join(classification['reasons'])}")
96
+ if classification.get("priority_indicators"):
97
+ lines.append(f" 推荐指标: {', '.join(classification['priority_indicators'])}")
98
+
99
+ # ── 缠论分析 ──
100
+ chan = features.get("chan_theory") or {}
101
+ if chan.get("valid"):
102
+ lines.append(f"\n## 缠论分析")
103
+ lines.append(f" 处理后K线: {chan.get('merged_count', '-')}/{chan.get('original_count', '-')}"
104
+ f" (合并率{chan.get('merge_ratio_pct', '-')}%)")
105
+ lines.append(f" 分型: {chan.get('fenxing_count', 0)} (顶{chan.get('top_fenxing', 0)}/底{chan.get('bottom_fenxing', 0)})"
106
+ f" → 笔: {chan.get('bi_count', 0)} (↑{chan.get('up_bi', 0)} ↓{chan.get('down_bi', 0)})"
107
+ f" → 线段: {chan.get('xianduan_count', 0)}")
108
+ zs_list = chan.get("zhongshu_list", [])
109
+ if zs_list:
110
+ zs_desc = "; ".join(f"[{z['zd']}-{z['zg']}]" for z in zs_list[-2:])
111
+ lines.append(f" 中枢({chan.get('zhongshu_count', 0)}): {zs_desc}")
112
+ beichi = chan.get("beichi", {})
113
+ if beichi.get("summary"):
114
+ lines.append(f" 背驰: {beichi['summary']}")
115
+ maidain = chan.get("maidian", {})
116
+ buy_pts = maidain.get("buy_points", [])
117
+ sell_pts = maidain.get("sell_points", [])
118
+ if buy_pts:
119
+ bp_desc = "; ".join(bp["type"] + "(" + bp.get("confidence", "") + ")" for bp in buy_pts)
120
+ lines.append(f" 买点: {bp_desc}")
121
+ if sell_pts:
122
+ sp_desc = "; ".join(sp["type"] + "(" + sp.get("confidence", "") + ")" for sp in sell_pts)
123
+ lines.append(f" 卖点: {sp_desc}")
124
+ if not buy_pts and not sell_pts:
125
+ lines.append(f" 买卖点: 当前无明确缠论买卖点")
126
+ lines.append(f" 当前位置: {chan.get('current_position', '-')}")
127
+
128
+ # ── A股本土战法 ──
129
+ local_p = features.get("local_patterns") or {}
130
+ if local_p.get("patterns"):
131
+ lines.append(f"\n## A股本土战法")
132
+ for lp in local_p["patterns"]:
133
+ icon = "↑" if lp["type"] == "看涨" else "↓"
134
+ lines.append(f" {icon} {lp['name']} ({lp['confidence']}): {lp['desc']}")
135
+ lines.append(f" {local_p.get('summary', '')}")
136
+ elif local_p:
137
+ lines.append(f"\n## A股本土战法\n {local_p.get('summary', '未检测到形态')}")
138
+
139
+ # ── 市场环境自适应 ──
140
+ market_env = features.get("market_environment") or {}
141
+ if market_env.get("state") and market_env["state"] != "震荡":
142
+ lines.append(f"\n## 市场环境自适应")
143
+ lines.append(f" 市场状态: {market_env['state']} (置信度: {market_env.get('confidence', '-')})")
144
+ adj_info = market_env.get("weight_adjustments", {})
145
+ if adj_info.get("desc"):
146
+ lines.append(f" 权重调整: {adj_info['desc']}")
147
+
148
+ # ── 支撑与阻力 ──
149
+ lines.append(f"\n## 支撑与阻力")
150
+ lines.append(f" {'支撑位':<10} {'来源':<8} {'强度'}")
151
+ for s in sr.get("supports", []):
152
+ lines.append(f" {s['level']:<10} {s['source']:<8} {s['strength']}")
153
+ lines.append(f" {'阻力位':<10} {'来源':<8} {'强度'}")
154
+ for r in sr.get("resistances", []):
155
+ lines.append(f" {r['level']:<10} {r['source']:<8} {r['strength']}")
156
+
157
+ # ── 趋势结构 ──
158
+ lines.append(f"\n## 趋势结构")
159
+ lines.append(f" 波浪状态: {wave}")
160
+ if box:
161
+ lines.append(f" 箱体: {box['top']}-{box['bottom']} 震荡 (幅度{box['range_pct']}%, {box['days']}日)")
162
+ if breakout and breakout.get("status", "未突破") != "未突破":
163
+ lines.append(f" 突破: {breakout.get('status')}")
164
+
165
+ # ── A 股特化 ──
166
+ if limit_data:
167
+ lines.append(f"\n## A股特化分析")
168
+ lines.append(f" 板块制度: {limit_data.get('board', '-')} (涨跌停{limit_data.get('limit_ratio', 10)}%)")
169
+ lines.append(f" 涨跌停价: 涨停{limit_data.get('limit_up_price', '-')} / 跌停{limit_data.get('limit_down_price', '-')}")
170
+ lines.append(f" 当前状态: {limit_data.get('board_status', '-')}")
171
+ if limit_data.get("limit_streak", 0) > 0:
172
+ lines.append(f" 连板: {limit_data.get('limit_streak')}连板 ({limit_data.get('streak_type')})")
173
+ if limit_data.get("streak_volume"):
174
+ lines.append(f" 连板量能: {limit_data['streak_volume']}")
175
+ if limit_data.get("t1_risk"):
176
+ lines.append(f" ⚠ {limit_data['t1_risk']}")
177
+
178
+ # ── 综合建议止损 ──
179
+ lines.append(f"\n## 仓位参考(技术面)")
180
+ nearest_support = sr.get("nearest_support")
181
+ if nearest_support:
182
+ stop_pct = round((meta['price_num'] - nearest_support) / meta['price_num'] * 100, 1)
183
+ lines.append(f" 止损位: {nearest_support} (距现价 -{abs(stop_pct)}%)")
184
+ nearest_resistance = sr.get("nearest_resistance")
185
+ if nearest_resistance:
186
+ tp_pct = round((nearest_resistance - meta['price_num']) / meta['price_num'] * 100, 1)
187
+ lines.append(f" 止盈位: {nearest_resistance} (距现价 +{tp_pct}%)")
188
+ lines.append(f" 纯技术视角,不构成投资建议。需结合基本面、市场环境综合判断。")
189
+
190
+ lines.append("═" * 72)
191
+ return "\n".join(lines)
192
+
193
+
194
+ def render_quick(features, score, meta):
195
+ """快速技术摘要。"""
196
+ ma = features.get("ma_system", {})
197
+ macd = features.get("macd") or {}
198
+ vol = features.get("volume") or {}
199
+ sr = features.get("support_resistance", {})
200
+ limit_data = features.get("limit_analysis") or {}
201
+
202
+ lines = []
203
+ lines.append(f"## 技术面快扫: {meta['name']} ({meta['code']})")
204
+ lines.append(f"现价: {meta['price']} | 涨跌: {meta['change_pct']}% | 板块: {meta['board']} | {meta['timestamp']}")
205
+ lines.append("")
206
+ lines.append(f"评分: {score['score']}/100 ({score['grade']})")
207
+ lines.append(f"趋势: {ma.get('alignment', '-')}")
208
+ macd_icon = "↑金叉" if macd.get('signal') == 1 else "↓死叉" if macd.get('signal') == -1 else "→"
209
+ lines.append(f"MACD: {macd_icon} | {macd.get('bar_trend', '-')}")
210
+ if macd.get("divergence"):
211
+ lines.append(f" ⚠ {macd['divergence']}")
212
+ lines.append(f"量能: {vol.get('volume_ratio_desc', '-')} | {vol.get('volume_price', '-')}")
213
+ lines.append(f"支撑: {sr.get('nearest_support', '-')} | 阻力: {sr.get('nearest_resistance', '-')}")
214
+ if limit_data and limit_data.get("limit_streak", 0) > 0:
215
+ lines.append(f"连板: {limit_data['limit_streak']}板 ({limit_data.get('board_status')})")
216
+ if score["buy_signals"]:
217
+ lines.append(f"买入: {', '.join(score['buy_signals'])}")
218
+ if score["sell_signals"]:
219
+ lines.append(f"卖出: {', '.join(score['sell_signals'])}")
220
+ lines.append(f"⚠ 纯技术视角,不构成投资建议")
221
+ return "\n".join(lines)
@@ -0,0 +1,37 @@
1
+ """
2
+ RSI 指标(Wilder 平滑方法)。
3
+ 无内部依赖。
4
+ """
5
+
6
+
7
+ def rsi_features(closes, period=14):
8
+ """RSI 计算(Wilder 指数平滑,与通达信/同花顺一致)。"""
9
+ if len(closes) < period + 1:
10
+ return {"rsi": 50, "signal": 0}
11
+
12
+ # 计算涨跌序列
13
+ gains, losses = [], []
14
+ for i in range(1, len(closes)):
15
+ chg = closes[i] - closes[i - 1]
16
+ gains.append(max(chg, 0))
17
+ losses.append(max(-chg, 0))
18
+
19
+ # Wilder 平滑:初始值用 SMA,后续用指数平滑
20
+ avg_gain = sum(gains[:period]) / period
21
+ avg_loss = sum(losses[:period]) / period
22
+ for i in range(period, len(gains)):
23
+ avg_gain = (avg_gain * (period - 1) + gains[i]) / period
24
+ avg_loss = (avg_loss * (period - 1) + losses[i]) / period
25
+
26
+ if avg_loss == 0:
27
+ rsi = 100
28
+ else:
29
+ rs = avg_gain / avg_loss
30
+ rsi = 100 - 100 / (1 + rs)
31
+
32
+ signal = 0
33
+ if rsi < 30:
34
+ signal = 1
35
+ elif rsi > 70:
36
+ signal = -1
37
+ return {"rsi": round(rsi, 1), "signal": signal}
@@ -0,0 +1,392 @@
1
+ """
2
+ 综合评分引擎和市场环境检测。
3
+ 依赖: common (to_float, clamp), signals (_generate_signals)
4
+ """
5
+ from common import clamp, to_float
6
+
7
+ from .signals import _generate_signals
8
+
9
+
10
+ # 个股类型 × 指标权重矩阵
11
+ _STOCK_TYPE_WEIGHTS = {
12
+ "题材股": {
13
+ "ma": 0.6, "macd": 0.5, "kdj": 0.5,
14
+ "boll": 0.8, "rsi": 1.0, "volume": 1.3,
15
+ "pattern": 1.5, "limit": 1.5, "chan": 0.5,
16
+ },
17
+ "蓝筹股": {
18
+ "ma": 1.3, "macd": 1.1, "kdj": 0.4,
19
+ "boll": 1.2, "rsi": 0.9, "volume": 0.8,
20
+ "pattern": 0.7, "limit": 0.3, "chan": 0.8,
21
+ },
22
+ "强成长股": {
23
+ "ma": 0.9, "macd": 1.3, "kdj": 0.4,
24
+ "boll": 1.2, "rsi": 0.9, "volume": 1.2,
25
+ "pattern": 0.8, "limit": 0.5, "chan": 0.7,
26
+ },
27
+ "周期股": {
28
+ "ma": 0.6, "macd": 1.3, "kdj": 1.2,
29
+ "boll": 1.0, "rsi": 0.9, "volume": 0.9,
30
+ "pattern": 0.7, "limit": 0.4, "chan": 1.3,
31
+ },
32
+ "稳成长股": {
33
+ "ma": 1.2, "macd": 1.1, "kdj": 0.5,
34
+ "boll": 1.0, "rsi": 1.0, "volume": 0.9,
35
+ "pattern": 1.0, "limit": 0.3, "chan": 0.8,
36
+ },
37
+ "防御股": {
38
+ "ma": 0.8, "macd": 0.9, "kdj": 0.6,
39
+ "boll": 1.1, "rsi": 1.1, "volume": 0.7,
40
+ "pattern": 0.7, "limit": 0.3, "chan": 0.9,
41
+ },
42
+ "普通股": {
43
+ "ma": 1.0, "macd": 1.0, "kdj": 1.0,
44
+ "boll": 1.0, "rsi": 1.0, "volume": 1.0,
45
+ "pattern": 1.0, "limit": 1.0, "chan": 1.0,
46
+ },
47
+ }
48
+
49
+
50
+ def composite_score(features, stock_type="普通股", market_state=None):
51
+ """自适应多指标共振评分 0-100,按个股类型和市场环境调整权重。"""
52
+ score = 0
53
+ ma = features.get("ma_system", {})
54
+ macd = features.get("macd") or {}
55
+ kdj = features.get("kdj") or {}
56
+ boll = features.get("bollinger") or {}
57
+ rsi_data = features.get("rsi", {})
58
+ vol = features.get("volume") or {}
59
+ patterns = features.get("patterns", [])
60
+
61
+ # 获取权重
62
+ type_w = _STOCK_TYPE_WEIGHTS.get(stock_type, _STOCK_TYPE_WEIGHTS["普通股"])
63
+ adj = {}
64
+ if market_state:
65
+ adj = _market_weight_adjustments(market_state)
66
+ else:
67
+ adj = _market_weight_adjustments("震荡")
68
+
69
+ # 1. 均线 20 分 × 类型权重 × 市场趋势权重
70
+ alignment = ma.get("alignment", "")
71
+ alignment_scores = {"多头排列": 20, "交叉震荡": 12, "空头排列": 3, "数据不足": 7}
72
+ ma_base = alignment_scores.get(alignment, 7)
73
+ ma_score = ma_base * type_w["ma"] * (adj.get("trend_following", 1.0) if alignment == "多头排列" else 1.0)
74
+ score += clamp(ma_score, 0, 30)
75
+
76
+ # 2. MACD 15 分(上限 20 分,下限 0 分)
77
+ macd_signal = macd.get("signal", 0)
78
+ bar_trend = macd.get("bar_trend", "")
79
+ divergence = macd.get("divergence", "")
80
+ macd_base = 7
81
+ if macd_signal == 1 and "放大" in bar_trend:
82
+ macd_base = 15
83
+ elif macd_signal == 1:
84
+ macd_base = 10
85
+ elif macd_signal == -1:
86
+ macd_base = 3
87
+ macd_score = macd_base * type_w["macd"]
88
+ if divergence == "底背离(看涨)":
89
+ macd_score += 8 * adj.get("divergence_bottom", 1.0)
90
+ elif divergence == "顶背离(看跌)":
91
+ macd_score -= 8 * adj.get("overbought", 1.0)
92
+ score += clamp(macd_score, 0, 20)
93
+
94
+ # 3. KDJ 10 分(辅助信号:仅在震荡市/盘整时生效,其他市场降权)
95
+ # KDJ 与 RSI 功能重叠(超买超卖),KDJ 更适合短线震荡
96
+ market_state_for_kdj = adj.get("trend_following", 1.0)
97
+ kdj_active = market_state_for_kdj < 1.0 # 震荡/熊市时 KDJ 更有效
98
+ kdj_weight = 5 if kdj.get("钝化") else (10 if kdj_active else 5)
99
+ kdj_sig = kdj.get("signal", "")
100
+ # 按关键词匹配评分(支持组合信号如"金叉+超卖"、"死叉+超买"等)
101
+ if "金叉" in kdj_sig and "超卖" in kdj_sig:
102
+ kdj_base = kdj_weight
103
+ elif "金叉" in kdj_sig:
104
+ kdj_base = kdj_weight * 0.8
105
+ elif "死叉" in kdj_sig and "超买" in kdj_sig:
106
+ kdj_base = kdj_weight * 0.1
107
+ elif "死叉" in kdj_sig:
108
+ kdj_base = kdj_weight * 0.2
109
+ elif "超卖" in kdj_sig:
110
+ kdj_base = kdj_weight * 0.6
111
+ elif "超买" in kdj_sig:
112
+ kdj_base = kdj_weight * 0.3
113
+ else:
114
+ kdj_base = kdj_weight * 0.45
115
+ kdj_score = kdj_base * type_w["kdj"]
116
+ score += clamp(kdj_score, 0, 15)
117
+
118
+ # 4. BOLL 10 分
119
+ pos = boll.get("position", 0.5)
120
+ bw = boll.get("bandwidth_desc", "")
121
+ boll_base = 7
122
+ if pos < 0.3 and "收窄" in bw:
123
+ boll_base = 10
124
+ elif 0.3 <= pos <= 0.7:
125
+ boll_base = 7
126
+ elif pos > 0.7:
127
+ boll_base = 4
128
+ else:
129
+ boll_base = 5
130
+ score += boll_base * type_w["boll"]
131
+
132
+ # 5. RSI 10 分
133
+ rsi = rsi_data.get("rsi", 50)
134
+ rsi_base = 7
135
+ if 30 <= rsi <= 40:
136
+ rsi_base = 10
137
+ elif 40 < rsi <= 60:
138
+ rsi_base = 7
139
+ elif 20 <= rsi < 30:
140
+ rsi_base = 8
141
+ elif 60 < rsi <= 70:
142
+ rsi_base = 5
143
+ elif rsi > 70:
144
+ rsi_base = 3
145
+ else:
146
+ rsi_base = 5
147
+ score += rsi_base * type_w["rsi"]
148
+
149
+ # 6. 成交量 15 分
150
+ vp_signal = vol.get("volume_price_signal", 0)
151
+ vr = vol.get("volume_ratio", 1)
152
+ vol_base = 7
153
+ if vp_signal == 1:
154
+ vol_base = 12
155
+ elif vp_signal == -1:
156
+ vol_base = 3
157
+ vol_score = vol_base * type_w["volume"]
158
+ if vr < 0.3:
159
+ vol_score += 3
160
+ score += clamp(vol_score, 0, 20)
161
+
162
+ # 7. K线形态 15 分 × 类型权重 × 市场牛市偏向
163
+ bullish_patterns = ["早晨之星", "阳包阴", "锤子线", "红三兵", "假阴真阳"]
164
+ bearish_patterns = ["黄昏之星", "阴包阳", "倒锤子", "三只乌鸦", "假阳真阴"]
165
+ pattern_score = 7
166
+ for p in patterns:
167
+ ptype = p.get("type", "")
168
+ if any(b in ptype for b in bullish_patterns):
169
+ pattern_score = max(pattern_score, 13)
170
+ if any(b in ptype for b in bearish_patterns):
171
+ pattern_score = min(pattern_score, 3)
172
+ score += clamp(pattern_score * type_w["pattern"] * adj.get("bullish_bias", 1.0), 0, 25)
173
+
174
+ # 8. 缠论加分项(上限 15 分)
175
+ chan_bonus = 0
176
+ chan_data = features.get("chan_theory") or {}
177
+ if chan_data.get("valid"):
178
+ maidain = chan_data.get("maidian", {})
179
+ buy_points = maidain.get("buy_points", [])
180
+ for bp in buy_points:
181
+ bpt = bp.get("type", "")
182
+ if bpt == "一买":
183
+ chan_bonus += 10 * adj.get("buy_point_1", 1.0)
184
+ elif bpt == "二买":
185
+ chan_bonus += 5
186
+ elif bpt == "三买":
187
+ chan_bonus += 8 * adj.get("buy_point_3", 1.0)
188
+ beichi = chan_data.get("beichi", {})
189
+ if beichi.get("summary", "").startswith("检测到底背驰"):
190
+ chan_bonus += 8 * adj.get("divergence_bottom", 1.0)
191
+ score += clamp(chan_bonus, 0, 15)
192
+
193
+ # 9. 本土战法加分(上限 10 分)
194
+ local_bonus = 0
195
+ local_patterns_data = features.get("local_patterns") or {}
196
+ for lp in local_patterns_data.get("patterns", []):
197
+ pname = lp.get("name", "")
198
+ pconf = lp.get("confidence", "中")
199
+ bonus = 0
200
+ if pname == "老鸭头":
201
+ bonus = 8
202
+ elif pname == "美人肩":
203
+ bonus = 6
204
+ elif pname == "三阴一阳":
205
+ bonus = 5
206
+ elif pname == "涨停双响炮":
207
+ bonus = 7
208
+ elif pname == "底部首板":
209
+ bonus = 6
210
+ elif pname == "双针探底":
211
+ bonus = 5
212
+ if pconf == "高":
213
+ bonus *= 1.2
214
+ local_bonus += bonus
215
+ score += clamp(local_bonus, 0, 10)
216
+
217
+ score = clamp(score, 0, 100)
218
+
219
+ # 定级(含模糊区间:边界附近标注"边界")
220
+ if score >= 80:
221
+ grade = "强烈看多"
222
+ elif score >= 75:
223
+ grade = "偏多(强)" # 模糊区间:75-80
224
+ elif score >= 60:
225
+ grade = "偏多"
226
+ elif score >= 55:
227
+ grade = "中性(偏多)" # 模糊区间:55-65
228
+ elif score >= 40:
229
+ grade = "中性"
230
+ elif score >= 35:
231
+ grade = "中性(偏空)" # 模糊区间:35-45
232
+ elif score >= 20:
233
+ grade = "偏空"
234
+ elif score >= 15:
235
+ grade = "偏空(强)" # 模糊区间:15-25
236
+ else:
237
+ grade = "强烈看空"
238
+
239
+ buy_signals, sell_signals = _generate_signals(features)
240
+
241
+ return {
242
+ "score": round(score, 1),
243
+ "grade": grade,
244
+ "buy_signals": buy_signals,
245
+ "sell_signals": sell_signals,
246
+ }
247
+
248
+
249
+ def detect_market_environment(index_quote=None, recent_quotes=None):
250
+ """
251
+ 检测当前市场环境(牛市/熊市/震荡/冰点/亢奋)。
252
+ 优先使用大盘数据(涨跌停家数),不可得时用指数技术指标推断。
253
+ 支持多日窗口判断,避免单日噪声。
254
+
255
+ Args:
256
+ index_quote: 大盘指数行情 dict(可选)
257
+ recent_quotes: 近期大盘行情列表(可选,用于多日均值判断)
258
+
259
+ Returns:
260
+ {
261
+ "state": "牛市"|"熊市"|"震荡"|"冰点"|"亢奋",
262
+ "confidence": "高"|"中"|"低",
263
+ "signals": [...],
264
+ "weight_adjustments": {...},
265
+ }
266
+ """
267
+ state = "震荡"
268
+ confidence = "低"
269
+ signals = []
270
+
271
+ if index_quote and isinstance(index_quote, dict):
272
+ price = to_float(index_quote.get("price"))
273
+ change_pct = to_float(index_quote.get("change_pct"))
274
+ turnover = to_float(index_quote.get("turnover"))
275
+
276
+ # 多日窗口:用近期数据的均值平滑单日噪声
277
+ if recent_quotes and len(recent_quotes) > 1:
278
+ recent_changes = [to_float(q.get("change_pct")) for q in recent_quotes]
279
+ recent_turnovers = [to_float(q.get("turnover")) for q in recent_quotes]
280
+ avg_change = sum(recent_changes) / len(recent_changes)
281
+ avg_turnover = sum(recent_turnovers) / len(recent_turnovers)
282
+ window_days = len(recent_quotes)
283
+ signals.append(f"近{window_days}日均涨跌{avg_change:.2f}%")
284
+ else:
285
+ avg_change = change_pct
286
+ avg_turnover = turnover
287
+
288
+ # 用多日均值判断趋势
289
+ if avg_change > 1.5:
290
+ state = "牛市"
291
+ confidence = "高" if avg_change > 2.5 else "中"
292
+ signals.append(f"持续上涨(均值{avg_change:.1f}%)")
293
+ elif avg_change < -1.5:
294
+ state = "熊市"
295
+ confidence = "高" if avg_change < -2.5 else "中"
296
+ signals.append(f"持续下跌(均值{avg_change:.1f}%)")
297
+ elif avg_change > 0.3:
298
+ state = "牛市"
299
+ confidence = "低"
300
+ signals.append(f"温和上涨(均值{avg_change:.1f}%)")
301
+ elif avg_change < -0.3:
302
+ state = "熊市"
303
+ confidence = "低"
304
+ signals.append(f"温和下跌(均值{avg_change:.1f}%)")
305
+ else:
306
+ signals.append(f"窄幅震荡(均值{avg_change:.1f}%)")
307
+
308
+ # 用当日数据补充极端信号
309
+ if change_pct > 2:
310
+ signals.append(f"当日大涨{change_pct:.1f}%")
311
+ elif change_pct < -2:
312
+ signals.append(f"当日大跌{change_pct:.1f}%")
313
+
314
+ if avg_turnover > 5:
315
+ signals.append("高换手率")
316
+ if state == "牛市":
317
+ state = "亢奋"
318
+ signals.append("亢奋信号")
319
+ elif avg_turnover < 0.5:
320
+ signals.append("极度缩量")
321
+ if state in ("熊市", "震荡"):
322
+ state = "冰点"
323
+ signals.append("冰点信号")
324
+ else:
325
+ signals.append("大盘数据缺失,默认震荡")
326
+
327
+ # 市场 → 信号权重调整
328
+ adjustments = _market_weight_adjustments(state)
329
+
330
+ return {
331
+ "state": state,
332
+ "confidence": confidence,
333
+ "signals": signals,
334
+ "weight_adjustments": adjustments,
335
+ }
336
+
337
+
338
+ def _market_weight_adjustments(state):
339
+ """市场环境 → 信号权重因子。"""
340
+ adjustments = {
341
+ "牛市": {
342
+ "bullish_bias": 1.3,
343
+ "trend_following": 1.4,
344
+ "breakout": 1.3,
345
+ "divergence_bottom": 0.5,
346
+ "buy_point_1": 0.5,
347
+ "buy_point_3": 1.3,
348
+ "overbought": 0.8,
349
+ "desc": "牛市:趋势跟随加权,底背离/一买降权",
350
+ },
351
+ "熊市": {
352
+ "bullish_bias": 1.5,
353
+ "trend_following": 0.6,
354
+ "breakout": 0.6,
355
+ "divergence_bottom": 1.5,
356
+ "buy_point_1": 1.5,
357
+ "buy_point_3": 0.5,
358
+ "overbought": 1.3,
359
+ "desc": "熊市:反转信号加权,追涨信号降权",
360
+ },
361
+ "震荡": {
362
+ "bullish_bias": 1.0,
363
+ "trend_following": 0.8,
364
+ "breakout": 0.8,
365
+ "divergence_bottom": 1.2,
366
+ "buy_point_1": 1.1,
367
+ "buy_point_3": 1.2,
368
+ "overbought": 1.0,
369
+ "desc": "震荡:反转+区间交易加权,趋势信号降权",
370
+ },
371
+ "冰点": {
372
+ "bullish_bias": 1.8,
373
+ "trend_following": 0.3,
374
+ "breakout": 0.4,
375
+ "divergence_bottom": 1.8,
376
+ "buy_point_1": 2.0,
377
+ "buy_point_3": 0.3,
378
+ "overbought": 1.5,
379
+ "desc": "冰点:极度超卖反转加权,趋势信号大幅降权",
380
+ },
381
+ "亢奋": {
382
+ "bullish_bias": 0.6,
383
+ "trend_following": 0.5,
384
+ "breakout": 0.5,
385
+ "divergence_bottom": 0.4,
386
+ "buy_point_1": 0.3,
387
+ "buy_point_3": 0.5,
388
+ "overbought": 0.3,
389
+ "desc": "亢奋:全面保守,警惕反转",
390
+ },
391
+ }
392
+ return adjustments.get(state, adjustments["震荡"])