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,591 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 缠中说禅理论(缠论)实现。
4
+ 包含:K线包含处理 → 分型 → 笔 → 线段 → 中枢 → 买卖点 → 背驰检测。
5
+ 用于 A 股技术分析,纯算法实现,不依赖外部数据。
6
+ """
7
+ import math
8
+ from common import to_float
9
+ from technical.core import _ema_series
10
+
11
+
12
+ # ═══════════════════════════════════════════════════════════════
13
+ # 1. K线包含处理
14
+ # ═══════════════════════════════════════════════════════════════
15
+
16
+ def chan_merge_inclusions(records, max_merge=3):
17
+ """
18
+ K线包含关系处理:连续涨势取高高,跌势取低低。
19
+ 返回合并后的 K 线列表。
20
+
21
+ Args:
22
+ records: K 线数据列表
23
+ max_merge: 最大连续合并次数(A 股涨跌停适配,默认 3,设为 0 表示不限制)
24
+ """
25
+ if len(records) < 3:
26
+ return records
27
+
28
+ bars = []
29
+ for r in records:
30
+ bars.append({
31
+ "high": to_float(r.get("high")),
32
+ "low": to_float(r.get("low")),
33
+ "open": to_float(r.get("open")),
34
+ "close": to_float(r.get("close")),
35
+ "date": r.get("day", ""),
36
+ "idx": len(bars),
37
+ })
38
+
39
+ merged = [dict(bars[0])]
40
+ direction = "up" # 默认向上
41
+ consecutive_merges = 0
42
+
43
+ for i in range(1, len(bars)):
44
+ prev = merged[-1]
45
+ curr = bars[i]
46
+
47
+ # 检查包含关系:一根K线的高低范围完全包含另一根
48
+ curr_in_prev = curr["high"] <= prev["high"] and curr["low"] >= prev["low"]
49
+ prev_in_curr = prev["high"] <= curr["high"] and prev["low"] >= curr["low"]
50
+
51
+ if (curr_in_prev or prev_in_curr) and (max_merge == 0 or consecutive_merges < max_merge):
52
+ consecutive_merges += 1
53
+ if direction == "up":
54
+ merged[-1] = {
55
+ "high": max(prev["high"], curr["high"]),
56
+ "low": max(prev["low"], curr["low"]),
57
+ "date": curr["date"],
58
+ "idx": curr["idx"],
59
+ }
60
+ else:
61
+ merged[-1] = {
62
+ "high": min(prev["high"], curr["high"]),
63
+ "low": min(prev["low"], curr["low"]),
64
+ "date": curr["date"],
65
+ "idx": curr["idx"],
66
+ }
67
+ else:
68
+ consecutive_merges = 0
69
+ # 更新方向
70
+ if curr["high"] > prev["high"]:
71
+ direction = "up"
72
+ elif curr["low"] < prev["low"]:
73
+ direction = "down"
74
+ merged.append(dict(curr))
75
+
76
+ return merged
77
+
78
+
79
+ # ═══════════════════════════════════════════════════════════════
80
+ # 2. 顶底分型识别
81
+ # ═══════════════════════════════════════════════════════════════
82
+
83
+ def chan_fenxing(merged_bars):
84
+ """
85
+ 从合并后的 K 线序列识别顶分型和底分型。
86
+ 顶分型:中间K线高点最高,且中间K线低点最高。
87
+ 底分型:中间K线低点最低,且中间K线高点最低。
88
+ """
89
+ if len(merged_bars) < 3:
90
+ return []
91
+
92
+ fenxing_list = []
93
+ for i in range(1, len(merged_bars) - 1):
94
+ b0, b1, b2 = merged_bars[i - 1], merged_bars[i], merged_bars[i + 1]
95
+
96
+ # 顶分型
97
+ if b1["high"] > b0["high"] and b1["high"] > b2["high"] and \
98
+ b1["low"] > b0["low"] and b1["low"] > b2["low"]:
99
+ fenxing_list.append({"type": "顶", "bar": b1, "idx": i})
100
+
101
+ # 底分型
102
+ elif b1["low"] < b0["low"] and b1["low"] < b2["low"] and \
103
+ b1["high"] < b0["high"] and b1["high"] < b2["high"]:
104
+ fenxing_list.append({"type": "底", "bar": b1, "idx": i})
105
+
106
+ # 去重:连续同类型分型保留最强的
107
+ deduped = []
108
+ for fx in fenxing_list:
109
+ if not deduped:
110
+ deduped.append(fx)
111
+ continue
112
+ last = deduped[-1]
113
+ if last["type"] == fx["type"]:
114
+ # 同类型:顶保留更高的,底保留更低的
115
+ if fx["type"] == "顶" and fx["bar"]["high"] > last["bar"]["high"]:
116
+ deduped[-1] = fx
117
+ elif fx["type"] == "底" and fx["bar"]["low"] < last["bar"]["low"]:
118
+ deduped[-1] = fx
119
+ else:
120
+ deduped.append(fx)
121
+
122
+ return deduped
123
+
124
+
125
+ # ═══════════════════════════════════════════════════════════════
126
+ # 3. 笔的构建
127
+ # ═══════════════════════════════════════════════════════════════
128
+
129
+ def chan_bi(merged_bars):
130
+ """
131
+ 从合并K线构建笔。
132
+ 相邻顶底分型之间至少要有1根独立K线(合并后)。
133
+ """
134
+ fenxing_list = chan_fenxing(merged_bars)
135
+ bi_list = []
136
+
137
+ if len(fenxing_list) < 2:
138
+ return bi_list
139
+
140
+ i = 0
141
+ while i < len(fenxing_list) - 1:
142
+ f0, f1 = fenxing_list[i], fenxing_list[i + 1]
143
+
144
+ # 必须交替
145
+ if f0["type"] == f1["type"]:
146
+ i += 1
147
+ continue
148
+
149
+ # 至少1根独立K线间隔
150
+ if f1["idx"] - f0["idx"] < 2:
151
+ i += 1
152
+ continue
153
+
154
+ direction = "up" if f0["type"] == "底" else "down"
155
+ bi_list.append({
156
+ "start": f0,
157
+ "end": f1,
158
+ "direction": direction,
159
+ "high": round(max(f0["bar"]["high"], f1["bar"]["high"]), 3),
160
+ "low": round(min(f0["bar"]["low"], f1["bar"]["low"]), 3),
161
+ "start_idx": f0["idx"],
162
+ "end_idx": f1["idx"],
163
+ })
164
+ i += 1
165
+
166
+ return bi_list
167
+
168
+
169
+ # ═══════════════════════════════════════════════════════════════
170
+ # 4. 线段构建
171
+ # ═══════════════════════════════════════════════════════════════
172
+
173
+ def chan_xianduan(bi_list):
174
+ """
175
+ 从笔构建线段。
176
+ 线段至少由3笔构成,前3笔必须有重叠区间。
177
+ 线段破坏判断基于初始重叠区间(前3笔),不随后续笔动态更新。
178
+ """
179
+ if len(bi_list) < 3:
180
+ return []
181
+
182
+ xd_list = []
183
+ i = 0
184
+ while i <= len(bi_list) - 3:
185
+ b0, b1, b2 = bi_list[i], bi_list[i + 1], bi_list[i + 2]
186
+
187
+ # 前三笔必须有重叠
188
+ overlap_high = min(b0["high"], b1["high"], b2["high"])
189
+ overlap_low = max(b0["low"], b1["low"], b2["low"])
190
+
191
+ if overlap_low >= overlap_high:
192
+ i += 1
193
+ continue
194
+
195
+ # 线段方向 = 第一笔方向
196
+ direction = b0["direction"]
197
+
198
+ # 固定参考点:线段起点的极值(不随后续笔更新)
199
+ if direction == "up":
200
+ seg_start_low = b0["low"]
201
+ else:
202
+ seg_start_high = b0["high"]
203
+
204
+ # 扩展线段:加入更多笔
205
+ j = i + 3
206
+ while j < len(bi_list):
207
+ next_bi = bi_list[j]
208
+ if direction == "up":
209
+ # 上升段:后续笔的 low 不能跌破线段起点的 low
210
+ if next_bi["low"] >= seg_start_low:
211
+ j += 1
212
+ else:
213
+ break
214
+ else:
215
+ # 下降段:后续笔的 high 不能突破线段起点的 high
216
+ if next_bi["high"] <= seg_start_high:
217
+ j += 1
218
+ else:
219
+ break
220
+
221
+ seg_bis = bi_list[i:j]
222
+ xd_list.append({
223
+ "direction": direction,
224
+ "bi_count": len(seg_bis),
225
+ "start_bi": i,
226
+ "end_bi": j - 1,
227
+ "high": round(max(b["high"] for b in seg_bis), 3),
228
+ "low": round(min(b["low"] for b in seg_bis), 3),
229
+ })
230
+ i = j
231
+
232
+ return xd_list
233
+
234
+
235
+ # ═══════════════════════════════════════════════════════════════
236
+ # 5. 中枢识别
237
+ # ═══════════════════════════════════════════════════════════════
238
+
239
+ def chan_zhongshu(xd_list):
240
+ """
241
+ 从线段列表识别中枢。
242
+ 中枢 = 连续3段线段的重叠区间:ZG = min(线段高点), ZD = max(线段低点)。
243
+ 相邻中枢有重叠时合并为扩展中枢。
244
+ """
245
+ if len(xd_list) < 3:
246
+ return []
247
+
248
+ zs_list = []
249
+ for i in range(len(xd_list) - 2):
250
+ x0, x1, x2 = xd_list[i], xd_list[i + 1], xd_list[i + 2]
251
+ zg = min(x0["high"], x1["high"], x2["high"])
252
+ zd = max(x0["low"], x1["low"], x2["low"])
253
+
254
+ if zd < zg:
255
+ zs_list.append({
256
+ "zg": round(zg, 3),
257
+ "zd": round(zd, 3),
258
+ "mid": round((zg + zd) / 2, 3),
259
+ "width": round(zg - zd, 3),
260
+ "xd_start": i,
261
+ "xd_end": i + 2,
262
+ })
263
+
264
+ # 合并重叠中枢
265
+ if len(zs_list) <= 1:
266
+ return zs_list
267
+
268
+ merged_zs = [zs_list[0]]
269
+ for zs in zs_list[1:]:
270
+ last = merged_zs[-1]
271
+ # 有重叠(新中枢的低点 < 旧中枢的高点)
272
+ if zs["zd"] < last["zg"] and zs["zg"] > last["zd"]:
273
+ merged_zs[-1] = {
274
+ "zg": round(max(last["zg"], zs["zg"]), 3),
275
+ "zd": round(min(last["zd"], zs["zd"]), 3),
276
+ "mid": round((max(last["zg"], zs["zg"]) + min(last["zd"], zs["zd"])) / 2, 3),
277
+ "width": round(max(last["zg"], zs["zg"]) - min(last["zd"], zs["zd"]), 3),
278
+ "xd_start": last["xd_start"],
279
+ "xd_end": zs["xd_end"],
280
+ }
281
+ else:
282
+ merged_zs.append(zs)
283
+
284
+ return merged_zs
285
+
286
+
287
+ # ═══════════════════════════════════════════════════════════════
288
+ # 6. MACD 面积计算(用于背驰检测)
289
+ # ═══════════════════════════════════════════════════════════════
290
+
291
+ def _macd_area(dif_series, dea_series, start_idx, end_idx):
292
+ """计算 MACD 柱面积 = Σ|DIF - DEA|,用于力度对比。"""
293
+ if start_idx < 0 or end_idx >= len(dif_series) or start_idx >= end_idx:
294
+ return 0
295
+ area = 0
296
+ for i in range(start_idx, end_idx + 1):
297
+ area += abs(dif_series[i] - dea_series[i])
298
+ return area
299
+
300
+
301
+ # ═══════════════════════════════════════════════════════════════
302
+ # 7. 背驰检测
303
+ # ═══════════════════════════════════════════════════════════════
304
+
305
+ def chan_beichi(bi_list, zs_list, closes):
306
+ """
307
+ 背驰检测。
308
+ 趋势背驰:比较两段同向走势的 MACD 面积,面积衰减+价格创极端=背驰。
309
+ 盘整背驰:比较中枢前后两段的力度。
310
+ """
311
+ if len(closes) < 34 or len(bi_list) < 4:
312
+ return {"trend_beichi": None, "range_beichi": [], "summary": "数据不足"}
313
+
314
+ # 计算 DIF/DEA 序列
315
+ ema12 = _ema_series(closes, 12)
316
+ ema26 = _ema_series(closes, 26)
317
+ min_len = min(len(ema12), len(ema26))
318
+ dif_series = [ema12[i] - ema26[i] for i in range(min_len)]
319
+ dea_series = _ema_series(dif_series, 9)
320
+
321
+ result = {"trend_beichi": None, "range_beichi": [], "summary": ""}
322
+
323
+ # ── 趋势背驰:比较最后两段同向笔的力度 ──
324
+ # 找最后两段下跌笔(底背驰)或上升笔(顶背驰)
325
+ down_bis = [bi for bi in bi_list if bi["direction"] == "down"]
326
+ up_bis = [bi for bi in bi_list if bi["direction"] == "up"]
327
+
328
+ # 底背驰:最后两段下跌笔,第二段价格更低但 MACD 面积更小
329
+ if len(down_bis) >= 2:
330
+ b1, b2 = down_bis[-2], down_bis[-1]
331
+ start1, end1 = b1["start_idx"], b1["end_idx"]
332
+ start2, end2 = b2["start_idx"], b2["end_idx"]
333
+
334
+ if end2 < len(dif_series) and end1 < len(dif_series):
335
+ area1 = _macd_area(dif_series, dea_series, min(start1, len(dif_series) - 1), min(end1, len(dif_series) - 1))
336
+ area2 = _macd_area(dif_series, dea_series, min(start2, len(dif_series) - 1), min(end2, len(dif_series) - 1))
337
+ if area2 < area1 and b2["low"] < b1["low"]:
338
+ result["trend_beichi"] = "底背驰(看涨)"
339
+
340
+ # 顶背驰:最后两段上升笔,第二段价格更高但 MACD 面积更小
341
+ if len(up_bis) >= 2 and result["trend_beichi"] is None:
342
+ b1, b2 = up_bis[-2], up_bis[-1]
343
+ start1, end1 = b1["start_idx"], b1["end_idx"]
344
+ start2, end2 = b2["start_idx"], b2["end_idx"]
345
+
346
+ if end2 < len(dif_series) and end1 < len(dif_series):
347
+ area1 = _macd_area(dif_series, dea_series, min(start1, len(dif_series) - 1), min(end1, len(dif_series) - 1))
348
+ area2 = _macd_area(dif_series, dea_series, min(start2, len(dif_series) - 1), min(end2, len(dif_series) - 1))
349
+ if area2 < area1 and b2["high"] > b1["high"]:
350
+ result["trend_beichi"] = "顶背驰(看跌)"
351
+
352
+ # ── 盘整背驰:检查每个中枢的进入段 vs 离开段 ──
353
+ for zs_idx, zs in enumerate(zs_list):
354
+ # 盘整背驰:中枢前后各有一段同向走势,比较 MACD 面积
355
+ # 进入段 = 中枢前最后一段走势(在中枢 xd_start 之前的笔)
356
+ # 离开段 = 中枢后第一段走势(在中枢 xd_end 之后的笔)
357
+ xd_start = zs.get("xd_start", 0)
358
+ xd_end = zs.get("xd_end", 0)
359
+
360
+ # 找进入段:xd_start 之前的最后一笔
361
+ entry_bi = None
362
+ for bi in reversed(bi_list):
363
+ if bi["end_idx"] <= xd_start:
364
+ entry_bi = bi
365
+ break
366
+
367
+ # 找离开段:xd_end 之后的第一笔
368
+ exit_bi = None
369
+ for bi in bi_list:
370
+ if bi["start_idx"] >= xd_end:
371
+ exit_bi = bi
372
+ break
373
+
374
+ if entry_bi and exit_bi:
375
+ e_start, e_end = entry_bi["start_idx"], entry_bi["end_idx"]
376
+ x_start, x_end = exit_bi["start_idx"], exit_bi["end_idx"]
377
+
378
+ if e_end < len(dif_series) and x_end < len(dif_series):
379
+ entry_area = _macd_area(dif_series, dea_series,
380
+ min(e_start, len(dif_series) - 1),
381
+ min(e_end, len(dif_series) - 1))
382
+ exit_area = _macd_area(dif_series, dea_series,
383
+ min(x_start, len(dif_series) - 1),
384
+ min(x_end, len(dif_series) - 1))
385
+
386
+ # 离开段面积 < 进入段面积 = 盘整背驰
387
+ if exit_area < entry_area * 0.8: # 允许 20% 容差
388
+ zs_mid = zs.get("mid", 0)
389
+ last_close = closes[-1]
390
+ if last_close > zs.get("zg", 0):
391
+ result["range_beichi"].append({
392
+ "zs_idx": zs_idx,
393
+ "type": "盘整背驰(看跌)",
394
+ "desc": f"中枢上方离开力度衰减(面积比{exit_area/max(entry_area,0.01):.2f})",
395
+ })
396
+ elif last_close < zs.get("zd", 0):
397
+ result["range_beichi"].append({
398
+ "zs_idx": zs_idx,
399
+ "type": "盘整背驰(看涨)",
400
+ "desc": f"中枢下方离开力度衰减(面积比{exit_area/max(entry_area,0.01):.2f})",
401
+ })
402
+
403
+ summary_parts = []
404
+ if result["trend_beichi"]:
405
+ summary_parts.append(result["trend_beichi"])
406
+ if result["range_beichi"]:
407
+ summary_parts.append(f"{len(result['range_beichi'])}个中枢盘整背驰")
408
+ if summary_parts:
409
+ result["summary"] = "检测到" + "、".join(summary_parts)
410
+ else:
411
+ result["summary"] = "当前无明确背驰信号"
412
+
413
+ return result
414
+
415
+
416
+ # ═══════════════════════════════════════════════════════════════
417
+ # 8. 三类买卖点识别
418
+ # ═══════════════════════════════════════════════════════════════
419
+
420
+ def chan_maidian(merged_bars, bi_list, zs_list, closes):
421
+ """
422
+ 识别缠论三类买卖点。
423
+ 一买:离开最后一个中枢后的底背驰结束点
424
+ 二买:一买后不创新低的次低点
425
+ 三买:突破中枢后,回踩不落入中枢的点
426
+ """
427
+ if not zs_list or not bi_list or len(closes) < 20:
428
+ return {"buy_points": [], "sell_points": [], "summary": "数据不足"}
429
+
430
+ last_zs = zs_list[-1]
431
+ last_close = closes[-1]
432
+ last_idx = len(closes) - 1
433
+
434
+ buy_points = []
435
+ sell_points = []
436
+
437
+ # ── 一买:离开中枢的底背驰段结束 ──
438
+ # 条件:价格在中枢下方 + 下跌笔的结束点 + 出现底背驰
439
+ if last_close < last_zs["zd"]:
440
+ down_bis = [b for b in bi_list if b["direction"] == "down"]
441
+ if down_bis:
442
+ last_down = down_bis[-1]
443
+ # 判断是否为背驰段结尾
444
+ if last_down["end_idx"] >= last_idx - 5:
445
+ buy_points.append({
446
+ "type": "一买",
447
+ "desc": f"离开中枢(ZD={last_zs['zd']})后底背驰结束",
448
+ "confidence": "中",
449
+ })
450
+
451
+ # ── 二买:一买后的次低点 ──
452
+ if buy_points and any(bp["type"] == "一买" for bp in buy_points):
453
+ # 查找一买发生后是否出现回调未破前低
454
+ recent_lows = [min(b["low"] for b in bi_list[-3:])]
455
+ if recent_lows and last_close > recent_lows[0] and last_close < last_zs["zd"]:
456
+ buy_points.append({
457
+ "type": "二买",
458
+ "desc": f"回踩未破前低({recent_lows[0]}), 中枢下方",
459
+ "confidence": "中",
460
+ })
461
+
462
+ # ── 三买:突破中枢后回踩不入 ──
463
+ above_zs = closes[-1] > last_zs["zg"]
464
+ recent_low = min(closes[-5:]) if len(closes) >= 5 else closes[-1]
465
+ if above_zs and recent_low > last_zs["zd"]:
466
+ # 判断是否有回踩动作:近期有低点接近中枢但不落入
467
+ near_zs = any(last_zs["zg"] < c < last_zs["zg"] * 1.03 for c in closes[-10:])
468
+ if near_zs:
469
+ buy_points.append({
470
+ "type": "三买",
471
+ "desc": f"突破中枢上沿(ZG={last_zs['zg']})后回踩不落入",
472
+ "confidence": "高" if last_close > last_zs["zg"] * 1.02 else "中",
473
+ })
474
+
475
+ # ── 卖点(对称逻辑) ──
476
+ if last_close > last_zs["zg"]:
477
+ up_bis = [b for b in bi_list if b["direction"] == "up"]
478
+ if up_bis:
479
+ last_up = up_bis[-1]
480
+ if last_up["end_idx"] >= last_idx - 5:
481
+ sell_points.append({
482
+ "type": "一卖",
483
+ "desc": f"离开中枢(ZG={last_zs['zg']})后顶背驰",
484
+ "confidence": "中",
485
+ })
486
+
487
+ summary_parts = []
488
+ if buy_points:
489
+ summary_parts.append(f"买点: {', '.join(bp['type'] for bp in buy_points)}")
490
+ if sell_points:
491
+ summary_parts.append(f"卖点: {', '.join(sp['type'] for sp in sell_points)}")
492
+ summary = "; ".join(summary_parts) if summary_parts else "当前无明确缠论买卖点"
493
+
494
+ return {
495
+ "buy_points": buy_points,
496
+ "sell_points": sell_points,
497
+ "summary": summary,
498
+ }
499
+
500
+
501
+ # ═══════════════════════════════════════════════════════════════
502
+ # 9. 顶层整合函数
503
+ # ═══════════════════════════════════════════════════════════════
504
+
505
+ def chan_full_analysis(records):
506
+ """一次调用返回完整缠论分析结果。"""
507
+ if len(records) < 30:
508
+ return {"error": "K线数量不足(<30),缠论分析不可靠", "valid": False}
509
+
510
+ # 提取价格数据
511
+ closes = [to_float(r.get("close")) for r in records if to_float(r.get("close")) > 0]
512
+
513
+ if len(closes) < 30:
514
+ return {"error": "有效K线不足", "valid": False}
515
+
516
+ # 1. 包含处理
517
+ merged = chan_merge_inclusions(records)
518
+ merge_ratio = (len(records) - len(merged)) / len(records) * 100 if records else 0
519
+
520
+ # 2. 分型
521
+ fenxing = chan_fenxing(merged)
522
+ top_fx = [f for f in fenxing if f["type"] == "顶"]
523
+ bottom_fx = [f for f in fenxing if f["type"] == "底"]
524
+
525
+ # 3. 笔
526
+ bi_list = chan_bi(merged)
527
+ up_bis = [b for b in bi_list if b["direction"] == "up"]
528
+ down_bis = [b for b in bi_list if b["direction"] == "down"]
529
+
530
+ # 4. 线段
531
+ xd_list = chan_xianduan(bi_list)
532
+
533
+ # 5. 中枢
534
+ zs_list = chan_zhongshu(xd_list)
535
+
536
+ # 6. 背驰
537
+ beichi = chan_beichi(bi_list, zs_list, closes)
538
+
539
+ # 7. 买卖点
540
+ maidain = chan_maidian(merged, bi_list, zs_list, closes)
541
+
542
+ # 8. 当前位置描述
543
+ last_close = closes[-1]
544
+ if zs_list:
545
+ last_zs = zs_list[-1]
546
+ if last_close > last_zs["zg"]:
547
+ position = f"中枢上方({last_zs['zg']}之上)"
548
+ elif last_close < last_zs["zd"]:
549
+ position = f"中枢下方({last_zs['zd']}之下)"
550
+ else:
551
+ position = f"中枢内部(ZG={last_zs['zg']}, ZD={last_zs['zd']})"
552
+ else:
553
+ position = "无中枢,处于原始走势中"
554
+
555
+ valid = len(bi_list) >= 3
556
+
557
+ return {
558
+ "valid": valid,
559
+ "merged_count": len(merged),
560
+ "original_count": len(records),
561
+ "merge_ratio_pct": round(merge_ratio, 1),
562
+ "fenxing_count": len(fenxing),
563
+ "top_fenxing": len(top_fx),
564
+ "bottom_fenxing": len(bottom_fx),
565
+ "bi_count": len(bi_list),
566
+ "up_bi": len(up_bis),
567
+ "down_bi": len(down_bis),
568
+ "xianduan_count": len(xd_list),
569
+ "zhongshu_list": zs_list,
570
+ "zhongshu_count": len(zs_list),
571
+ "beichi": beichi,
572
+ "maidian": maidain,
573
+ "current_position": position,
574
+ }
575
+
576
+
577
+ # ── 命令行快速测试 ──
578
+ if __name__ == "__main__":
579
+ import sys
580
+ import json
581
+ from common import normalize_quote_code
582
+ from kline import fetch as fetch_kline
583
+
584
+ if len(sys.argv) < 2:
585
+ print("用法: python3 chan.py <code>")
586
+ sys.exit(1)
587
+
588
+ code = normalize_quote_code(sys.argv[1])
589
+ records = fetch_kline(code, 240, 250)
590
+ result = chan_full_analysis(records)
591
+ print(json.dumps(result, ensure_ascii=False, indent=2, default=str))