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,599 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ A 股本土战法形态识别。
4
+ 包含:三阴一阳、老鸭头、美人肩、双针探底、涨停双响炮、底部首板。
5
+ 纯技术形态识别,不依赖财务数据。
6
+ """
7
+ import math
8
+ from common import to_float, board_type as _board_type
9
+
10
+
11
+ # ═══════════════════════════════════════════════════════════════
12
+ # 工具函数
13
+ # ═══════════════════════════════════════════════════════════════
14
+
15
+ def _sma(values, period):
16
+ """简单移动平均。"""
17
+ if len(values) < period:
18
+ return []
19
+ result = []
20
+ for i in range(period - 1, len(values)):
21
+ result.append(sum(values[i - period + 1:i + 1]) / period)
22
+ return result
23
+
24
+
25
+ def _ema(values, period):
26
+ """指数移动平均。"""
27
+ if len(values) < period:
28
+ return []
29
+ k = 2 / (period + 1)
30
+ result = [sum(values[:period]) / period]
31
+ for v in values[period:]:
32
+ result.append(v * k + result[-1] * (1 - k))
33
+ return result
34
+
35
+
36
+ def _is_bearish(open_p, close_p):
37
+ """阴线:收盘低于开盘。"""
38
+ return close_p < open_p
39
+
40
+
41
+ def _is_bullish(open_p, close_p):
42
+ """阳线:收盘高于开盘。"""
43
+ return close_p >= open_p
44
+
45
+
46
+ def _lower_shadow(open_p, close_p, low_p):
47
+ """下影线长度/实体比例。"""
48
+ body_low = min(open_p, close_p)
49
+ shadow = body_low - low_p
50
+ body = abs(close_p - open_p)
51
+ return shadow / max(body, 0.001)
52
+
53
+
54
+ def _upper_shadow(open_p, close_p, high_p):
55
+ """上影线长度/实体比例。"""
56
+ body_high = max(open_p, close_p)
57
+ shadow = high_p - body_high
58
+ body = abs(close_p - open_p)
59
+ return shadow / max(body, 0.001)
60
+
61
+
62
+ def _body_pct(open_p, close_p):
63
+ """实体涨跌幅百分比。"""
64
+ return (close_p - open_p) / max(open_p, 0.001) * 100
65
+
66
+
67
+ def _is_limit_up(open_p, close_p, prev_close, board):
68
+ """检测涨停(考虑板块涨跌幅限制)。"""
69
+ limit_ratio = {"主板": 9.5, "创业板": 19.5, "科创板": 19.5, "北交所": 29.5}.get(board, 9.5)
70
+ chg = (close_p - prev_close) / max(prev_close, 0.001) * 100
71
+ return chg >= limit_ratio * 0.95
72
+
73
+
74
+ # ═══════════════════════════════════════════════════════════════
75
+ # 1. 三阴一阳 / 三阳一阴
76
+ # ═══════════════════════════════════════════════════════════════
77
+
78
+ def detect_sanying_yiyang(records, volumes, code=""):
79
+ """
80
+ 三阴一阳(洗盘后拉升)和三阳一阴(诱多后出货)。
81
+ 三阴一阳:连续3根阴线(缩量递减)→ 1根阳线覆盖前阴(放量)。
82
+ 三阳一阴:连续3根阳线(缩量递减)→ 1根阴线覆盖前阳(放量)。
83
+ """
84
+ if len(records) < 4:
85
+ return []
86
+
87
+ results = []
88
+
89
+ for i in range(3, len(records)):
90
+ r0, r1, r2, r3 = records[i - 3], records[i - 2], records[i - 1], records[i]
91
+ o0, c0, o1, c1, o2, c2, o3, c3 = [to_float(r.get(k)) for r in [r0, r1, r2, r3]
92
+ for k in ["open", "close"]]
93
+ v0, v1, v2, v3 = [to_float(r.get("volume")) for r in [r0, r1, r2, r3]]
94
+
95
+ # ── 三阴一阳(底部洗盘结束)──
96
+ if all(_is_bearish(o, c) for o, c in [(o0, c0), (o1, c1), (o2, c2)]) and _is_bullish(o3, c3):
97
+ # 三阴实体递减
98
+ body0, body1, body2 = abs(c0 - o0), abs(c1 - o1), abs(c2 - o2)
99
+ if body2 < body1 < body0:
100
+ # 阳线收盘覆盖至少前2根阴线收盘
101
+ if c3 > c1 and c3 > c0:
102
+ # 放量
103
+ if v3 > max(v0, v1, v2) * 1.3:
104
+ # 计算覆盖力度
105
+ coverage = (c3 - c0) / max(abs(c0 - o0), 0.001)
106
+ confidence = "高" if coverage > 0.8 and v3 > max(v0, v1, v2) * 1.5 else "中"
107
+ results.append({
108
+ "name": "三阴一阳",
109
+ "type": "看涨",
110
+ "date": r3.get("day", ""),
111
+ "desc": f"连续3阴缩量洗盘后放量阳线覆盖,覆盖力度{coverage:.1%}",
112
+ "confidence": confidence,
113
+ "idx": i,
114
+ })
115
+
116
+ # ── 三阳一阴(高位诱多出货)──
117
+ if all(_is_bullish(o, c) for o, c in [(o0, c0), (o1, c1), (o2, c2)]) and _is_bearish(o3, c3):
118
+ body0, body1, body2 = abs(c0 - o0), abs(c1 - o1), abs(c2 - o2)
119
+ if body2 < body1 < body0:
120
+ if c3 < c1 and c3 < c0:
121
+ if v3 > max(v0, v1, v2) * 1.3:
122
+ results.append({
123
+ "name": "三阳一阴",
124
+ "type": "看跌",
125
+ "date": r3.get("day", ""),
126
+ "desc": "连续3阳缩量上涨后放量阴线吞没",
127
+ "confidence": "中",
128
+ "idx": i,
129
+ })
130
+
131
+ return results
132
+
133
+
134
+ # ═══════════════════════════════════════════════════════════════
135
+ # 2. 老鸭头
136
+ # ═══════════════════════════════════════════════════════════════
137
+
138
+ def detect_laoyatou(records, closes, volumes, mas):
139
+ """
140
+ 老鸭头形态:三阶段(鸭颈 → 鸭头 → 鸭嘴)。
141
+ 鸭颈:MA5 > MA10 > MA20,股价沿MA5上行
142
+ 鸭头:MA5下穿MA10形成凹坑,股价回调但未跌破MA60
143
+ 鸭嘴:MA5重新上穿MA10,放量突破前高
144
+ """
145
+ if len(closes) < 60 or "ma5" not in mas or "ma10" not in mas or "ma20" not in mas or "ma60" not in mas:
146
+ return []
147
+
148
+ ma5, ma10, ma20, ma60 = mas["ma5"], mas["ma10"], mas["ma20"], mas["ma60"]
149
+
150
+ # 最小长度
151
+ if len(ma60) < 20:
152
+ return []
153
+
154
+ # 统一用 ma60 长度作为基准(最短的 MA),对齐索引
155
+ # ma60 对应 closes[59:], ma20 对应 closes[19:]
156
+ # 在 ma60 索引空间工作,closes 偏移 = 59
157
+ cl_offset = len(closes) - len(ma60)
158
+ ma5_offset = len(ma5) - len(ma60)
159
+ ma10_offset = len(ma10) - len(ma60)
160
+ ma20_offset = len(ma20) - len(ma60)
161
+
162
+ results = []
163
+
164
+ # 扫描鸭嘴形成点(MA5 刚上穿 MA10),从 ma60 空间找
165
+ for i_ma60 in range(20, len(ma60)):
166
+ i5 = i_ma60 + ma5_offset
167
+ i10 = i_ma60 + ma10_offset
168
+ ci = i_ma60 + cl_offset
169
+
170
+ if i5 < 1 or ci >= len(closes):
171
+ continue
172
+
173
+ # MA5 刚金叉 MA10
174
+ if not (ma5[i5 - 1] <= ma10[i10 - 1] and ma5[i5] > ma10[i10]):
175
+ continue
176
+
177
+ # 回溯找鸭头:前 5-15 天内 MA5/10 下穿点
178
+ duck_head_j = None
179
+ for j_ma60 in range(max(i_ma60 - 15, 5), i_ma60 - 2):
180
+ j5 = j_ma60 + ma5_offset
181
+ j10 = j_ma60 + ma10_offset
182
+ if j5 < 1:
183
+ continue
184
+ if ma5[j5] < ma10[j10] and ma5[j5] < ma5[j5 - 1]:
185
+ # 确保鸭头时股价在 MA60 上方
186
+ j_ci = j_ma60 + cl_offset
187
+ if closes[j_ci] > ma60[j_ma60] * 0.95:
188
+ duck_head_j = j_ma60
189
+ break
190
+
191
+ if duck_head_j is None:
192
+ continue
193
+
194
+ # 验证鸭颈:鸭头之前的上升趋势(MA5 > MA10 > MA20 至少3天)
195
+ neck_days = 0
196
+ for k_ma60 in range(max(duck_head_j - 10, 0), duck_head_j - 1):
197
+ k5 = k_ma60 + ma5_offset
198
+ k10 = k_ma60 + ma10_offset
199
+ k20 = k_ma60 + ma20_offset
200
+ if k20 >= 0 and k5 < len(ma5) and k20 < len(ma20):
201
+ if ma5[k5] > ma10[k10] > ma20[k20]:
202
+ neck_days += 1
203
+
204
+ if neck_days < 3:
205
+ continue
206
+
207
+ # 验证鸭嘴:放量 + 突破前高
208
+ head_ci = duck_head_j + cl_offset
209
+ lookback = min(head_ci, 10)
210
+ prev_high = max(closes[head_ci - lookback:head_ci]) if lookback >= 1 else closes[head_ci]
211
+
212
+ vol_recent = [volumes[k] for k in range(max(ci - 3, 0), ci + 1)]
213
+ vol_older = [volumes[k] for k in range(max(head_ci - 3, 0), head_ci + 1)]
214
+ avg_recent = sum(vol_recent) / max(len(vol_recent), 1)
215
+ avg_older = sum(vol_older) / max(len(vol_older), 1)
216
+ vol_expanding = avg_recent > avg_older * 1.2
217
+
218
+ breakout = closes[ci] > prev_high * 1.02
219
+
220
+ if vol_expanding and breakout:
221
+ confidence = "高" if closes[ci] > prev_high * 1.05 else "中"
222
+ results.append({
223
+ "name": "老鸭头",
224
+ "type": "看涨",
225
+ "date": records[ci].get("day", ""),
226
+ "desc": f"鸭嘴确认:MA5重上MA10+放量突破前高{prev_high:.2f}",
227
+ "confidence": confidence,
228
+ "idx": ci,
229
+ })
230
+
231
+ return results
232
+
233
+
234
+ # ═══════════════════════════════════════════════════════════════
235
+ # 3. 美人肩
236
+ # ═══════════════════════════════════════════════════════════════
237
+
238
+ def detect_meirenjian(records, closes, highs, lows, volumes, mas):
239
+ """
240
+ 美人肩:强势上升后 2-5 日横盘(不破 MA10)+ 缩量后放量突破。
241
+ 必须在上升趋势确认的前提下(股价在 MA5/MA10 上方)。
242
+ """
243
+ if len(closes) < 20 or "ma5" not in mas or "ma10" not in mas:
244
+ return []
245
+
246
+ ma5 = mas["ma5"]
247
+ ma10 = mas["ma10"]
248
+
249
+ # 统一用 ma10 长度作为基准(较短的那个)
250
+ base_len = min(len(ma5), len(ma10))
251
+ if base_len < 15:
252
+ return []
253
+
254
+ offset5 = len(ma5) - base_len
255
+ offset10 = len(ma10) - base_len
256
+ cl_offset = len(closes) - base_len
257
+
258
+ results = []
259
+
260
+ for i_base in range(14, base_len):
261
+ i5 = i_base + offset5
262
+ i10 = i_base + offset10
263
+ ci = i_base + cl_offset
264
+ if ci < 14 or ci >= len(closes):
265
+ continue
266
+
267
+ # 条件1:横盘前为上升趋势(过去5天 MA5斜率 > 0)
268
+ pre_slope = ma5[i5 - 5] - ma5[i5 - 10] if i5 >= 10 else 0
269
+ if pre_slope <= 0:
270
+ continue
271
+
272
+ # 条件2:最近 2-5 天横盘(价格振幅 2-5%,不破 MA10)
273
+ consolidation_range = range(max(ci - 5, 0), ci)
274
+ price_high = max(highs[j] for j in consolidation_range) if consolidation_range else closes[ci]
275
+ price_low = min(lows[j] for j in consolidation_range) if consolidation_range else closes[ci]
276
+ amplitude = (price_high - price_low) / max(price_low, 0.001) * 100
277
+
278
+ if not (2 <= amplitude <= 5):
279
+ continue
280
+
281
+ # 横盘期间不破 MA10
282
+ if price_low < ma10[i10]:
283
+ continue
284
+
285
+ # 条件3:横盘期间缩量(vs 横盘前5天)
286
+ consol_vol = [volumes[j] for j in consolidation_range]
287
+ pre_vol = [volumes[j] for j in range(max(ci - 10, 0), max(ci - 5, 0))]
288
+ if not consol_vol or not pre_vol:
289
+ continue
290
+ if sum(consol_vol) / len(consol_vol) > sum(pre_vol) / max(len(pre_vol), 1) * 0.7:
291
+ continue
292
+
293
+ # 条件4:今日放量突破横盘区间
294
+ if volumes[ci] > sum(consol_vol) / len(consol_vol) * 1.5 and closes[ci] > price_high:
295
+ results.append({
296
+ "name": "美人肩",
297
+ "type": "看涨",
298
+ "date": records[ci].get("day", ""),
299
+ "desc": f"横盘{len(consolidation_range)}日振幅{amplitude:.1f}%不破MA10后放量突破",
300
+ "confidence": "高" if volumes[ci] > sum(consol_vol) / len(consol_vol) * 2 else "中",
301
+ "idx": ci,
302
+ })
303
+
304
+ return results
305
+
306
+
307
+ # ═══════════════════════════════════════════════════════════════
308
+ # 4. 双针探底
309
+ # ═══════════════════════════════════════════════════════════════
310
+
311
+ def detect_shuangzhen(records, closes, lows, volumes):
312
+ """
313
+ 双针探底:5 日内两根长下影线触及相近价位 + 缩量。
314
+ 长下影标准:下影线 > 实体 × 2 或 > 上影线 × 3。
315
+ """
316
+ if len(records) < 5:
317
+ return []
318
+
319
+ results = []
320
+
321
+ for i in range(5, len(records)):
322
+ window = records[i - 5:i + 1]
323
+ w_lows = lows[i - 5:i + 1]
324
+ w_vol = volumes[i - 5:i + 1]
325
+
326
+ # 找长下影线
327
+ needle_days = []
328
+ for j, r in enumerate(window):
329
+ o, c, l, h = to_float(r.get("open")), to_float(r.get("close")), \
330
+ to_float(r.get("low")), to_float(r.get("high"))
331
+ body = abs(c - o)
332
+ lower = min(o, c) - l
333
+ upper = h - max(o, c)
334
+ if lower > body * 2 and lower > upper * 3 and body > 0:
335
+ needle_days.append({"idx": i - 5 + j, "low": l, "shadow": lower})
336
+
337
+ # 至少2根长下影,低点接近(<2% 差异)
338
+ if len(needle_days) >= 2:
339
+ for a in range(len(needle_days)):
340
+ for b in range(a + 1, len(needle_days)):
341
+ na, nb = needle_days[a], needle_days[b]
342
+ if nb["idx"] - na["idx"] < 1:
343
+ continue
344
+ low_diff = abs(na["low"] - nb["low"]) / max(na["low"], 0.001) * 100
345
+ if low_diff < 2:
346
+ # 确认缩量
347
+ vol_needle = [volumes[na["idx"]], volumes[nb["idx"]]]
348
+ vol_others = [v for j, v in enumerate(w_vol)
349
+ if i - 5 + j not in (na["idx"], nb["idx"])]
350
+ if sum(vol_needle) / 3 < sum(vol_others) / max(len(vol_others), 1) * 0.8:
351
+ results.append({
352
+ "name": "双针探底",
353
+ "type": "看涨",
354
+ "date": records[nb["idx"]].get("day", ""),
355
+ "desc": f"两低点{na['low']:.2f}/{nb['low']:.2f}差异{low_diff:.1f}%,缩量触底",
356
+ "confidence": "高" if low_diff < 1 else "中",
357
+ "idx": nb["idx"],
358
+ })
359
+
360
+ return results
361
+
362
+
363
+ # ═══════════════════════════════════════════════════════════════
364
+ # 5. 涨停双响炮
365
+ # ═══════════════════════════════════════════════════════════════
366
+
367
+ def detect_zhangting(records, closes, volumes, code=""):
368
+ """
369
+ 涨停双响炮:涨停 → 1-3 日缩量整理 → 再次涨停放量。
370
+ 用于确认强势股的二次攻击信号。
371
+ """
372
+ if len(records) < 5:
373
+ return []
374
+
375
+ board = _board_type(code) if code else "主板"
376
+
377
+ results = []
378
+
379
+ for i in range(4, len(records)):
380
+ r_now = records[i]
381
+ o_now, c_now, v_now = to_float(r_now.get("open")), to_float(r_now.get("close")), to_float(r_now.get("volume"))
382
+
383
+ # 当天必须是涨停
384
+ prev_close_now = to_float(records[i - 1].get("close"))
385
+ if not _is_limit_up(o_now, c_now, prev_close_now, board):
386
+ continue
387
+
388
+ # 回溯 1-3 天整理
389
+ for gap in range(1, 4):
390
+ zt1_idx = i - gap - 1
391
+ if zt1_idx < 0:
392
+ continue
393
+
394
+ r_zt1 = records[zt1_idx]
395
+ o1, c1, v1 = to_float(r_zt1.get("open")), to_float(r_zt1.get("close")), to_float(r_zt1.get("volume"))
396
+ prev_close_1 = to_float(records[zt1_idx - 1].get("close")) if zt1_idx > 0 else c1
397
+
398
+ # 第一次涨停
399
+ if not _is_limit_up(o1, c1, prev_close_1, board):
400
+ continue
401
+
402
+ # 中间整理期:缩量 + 收盘不破第一次涨停实体中点
403
+ zt1_mid = (o1 + c1) / 2
404
+ consolidation_ok = True
405
+ for k in range(zt1_idx + 1, i):
406
+ vk = volumes[k]
407
+ ck = closes[k]
408
+ if vk > v1 * 0.6 or ck < zt1_mid:
409
+ consolidation_ok = False
410
+ break
411
+
412
+ if not consolidation_ok:
413
+ continue
414
+
415
+ # 第二次涨停比第一次放量
416
+ if v_now > v1 * 1.2:
417
+ results.append({
418
+ "name": "涨停双响炮",
419
+ "type": "看涨",
420
+ "date": r_now.get("day", ""),
421
+ "desc": f"首板{gap + 1}日前+{gap}日缩量整理+今日再封板放量",
422
+ "confidence": "高" if gap == 1 and v_now > v1 * 1.5 else "中",
423
+ "idx": i,
424
+ })
425
+ break
426
+
427
+ return results
428
+
429
+
430
+ # ═══════════════════════════════════════════════════════════════
431
+ # 6. 底部首板
432
+ # ═══════════════════════════════════════════════════════════════
433
+
434
+ def detect_dibu_shouban(records, closes, highs, lows, volumes, code=""):
435
+ """
436
+ 底部首板:下跌趋势后首个涨停 → 2-3 日缩量回踩不破涨停日低点 → 确认。
437
+ """
438
+ if len(records) < 20:
439
+ return []
440
+
441
+ board = _board_type(code) if code else "主板"
442
+
443
+ results = []
444
+
445
+ for i in range(10, len(records) - 3):
446
+ r_zt = records[i]
447
+ o_zt, c_zt, h_zt, l_zt = [to_float(r_zt.get(k)) for k in ["open", "close", "high", "low"]]
448
+ v_zt = to_float(r_zt.get("volume"))
449
+ prev_close = to_float(records[i - 1].get("close"))
450
+
451
+ # 当天涨停
452
+ if not _is_limit_up(o_zt, c_zt, prev_close, board):
453
+ continue
454
+
455
+ # 涨停前处于下跌趋势(过去10天最高价低于20天前的高点)
456
+ recent_high = max(highs[i - 10:i]) if i >= 10 else highs[i]
457
+ older_high = max(highs[max(i - 20, 0):i - 10]) if i >= 20 else recent_high
458
+ if recent_high > older_high * 0.95:
459
+ continue
460
+
461
+ # 涨停前 5 天有过下跌
462
+ pre_change = (closes[i - 1] - closes[i - 5]) / max(closes[i - 5], 0.001) * 100 if i >= 5 else 0
463
+ if pre_change > -5:
464
+ continue
465
+
466
+ # 未来 2-3 日回踩:缩量 + 收盘不破涨停日低点
467
+ backtest_ok = True
468
+ min_vol = float("inf")
469
+ for k in range(i + 1, min(i + 4, len(records))):
470
+ if closes[k] < l_zt * 0.98:
471
+ backtest_ok = False
472
+ break
473
+ min_vol = min(min_vol, volumes[k])
474
+
475
+ if not backtest_ok:
476
+ continue
477
+
478
+ # 缩量确认(回踩期间均量 < 涨停日量 × 0.5)
479
+ if min_vol < v_zt * 0.5:
480
+ # 确认点:缩量后出现阳线
481
+ for k in range(i + 1, min(i + 4, len(records))):
482
+ rk = records[k]
483
+ ok, ck = to_float(rk.get("open")), to_float(rk.get("close"))
484
+ if _is_bullish(ok, ck) and volumes[k] > min_vol * 1.2:
485
+ results.append({
486
+ "name": "底部首板",
487
+ "type": "看涨",
488
+ "date": rk.get("day", ""),
489
+ "desc": f"下跌后首板+{k - i}日缩量回踩不破涨停低点{l_zt:.2f}",
490
+ "confidence": "高" if k - i <= 2 else "中",
491
+ "idx": k,
492
+ })
493
+ break
494
+
495
+ return results
496
+
497
+
498
+ # ═══════════════════════════════════════════════════════════════
499
+ # 顶层整合
500
+ # ═══════════════════════════════════════════════════════════════
501
+
502
+ def detect_all_local_patterns(records, closes, highs, lows, volumes, mas, code=""):
503
+ """
504
+ 运行所有本土战法形态识别,返回汇总结果。
505
+
506
+ Args:
507
+ records: K 线数据 list
508
+ closes: 收盘价序列
509
+ highs: 最高价序列
510
+ lows: 最低价序列
511
+ volumes: 成交量序列
512
+ mas: 移动平均线 dict {"ma5": [...], "ma10": [...], "ma20": [...], "ma60": [...]}
513
+ code: 股票代码(用于板块判断)
514
+
515
+ Returns:
516
+ {
517
+ "patterns": [{"name": ..., "type": ..., "date": ..., "desc": ..., "confidence": ...}],
518
+ "summary": "...",
519
+ "count": N,
520
+ }
521
+ """
522
+ all_patterns = []
523
+
524
+ # 三阴一阳/三阳一阴
525
+ all_patterns.extend(detect_sanying_yiyang(records, volumes, code))
526
+
527
+ # 老鸭头
528
+ all_patterns.extend(detect_laoyatou(records, closes, volumes, mas))
529
+
530
+ # 美人肩
531
+ all_patterns.extend(detect_meirenjian(records, closes, highs, lows, volumes, mas))
532
+
533
+ # 双针探底
534
+ all_patterns.extend(detect_shuangzhen(records, closes, lows, volumes))
535
+
536
+ # 涨停双响炮
537
+ all_patterns.extend(detect_zhangting(records, closes, volumes, code))
538
+
539
+ # 底部首板
540
+ all_patterns.extend(detect_dibu_shouban(records, closes, highs, lows, volumes, code))
541
+
542
+ # 按时间排序(最新的在后)
543
+ all_patterns.sort(key=lambda p: p["idx"])
544
+
545
+ # 去重:同一日期同一形态只保留一次
546
+ seen = set()
547
+ deduped = []
548
+ for p in all_patterns:
549
+ key = (p["name"], p["date"])
550
+ if key not in seen:
551
+ seen.add(key)
552
+ deduped.append(p)
553
+
554
+ # 只看最近出现的(idx 最大的)
555
+ recent = deduped[-5:] if len(deduped) > 5 else deduped
556
+
557
+ bullish = [p["name"] for p in recent if p["type"] == "看涨"]
558
+ bearish = [p["name"] for p in recent if p["type"] == "看跌"]
559
+
560
+ summary_parts = []
561
+ if bullish:
562
+ summary_parts.append(f"看涨形态: {', '.join(bullish)}")
563
+ if bearish:
564
+ summary_parts.append(f"看跌形态: {', '.join(bearish)}")
565
+ summary = "; ".join(summary_parts) if summary_parts else "未检测到本土战法形态"
566
+
567
+ return {
568
+ "patterns": recent,
569
+ "summary": summary,
570
+ "count": len(recent),
571
+ }
572
+
573
+
574
+ # ── 命令行快速测试 ──
575
+ if __name__ == "__main__":
576
+ import sys
577
+ import json
578
+ from common import normalize_quote_code
579
+ from kline import fetch as fetch_kline
580
+
581
+ if len(sys.argv) < 2:
582
+ print("用法: python3 patterns_local.py <code>")
583
+ sys.exit(1)
584
+
585
+ code = normalize_quote_code(sys.argv[1])
586
+ records = fetch_kline(code, 240, 250)
587
+
588
+ closes = [to_float(r.get("close")) for r in records if to_float(r.get("close")) > 0]
589
+ highs = [to_float(r.get("high")) for r in records]
590
+ lows = [to_float(r.get("low")) for r in records]
591
+ volumes = [to_float(r.get("volume")) for r in records]
592
+
593
+ mas = {}
594
+ for p in [5, 10, 20, 60]:
595
+ sma = _sma(closes, p)
596
+ mas[f"ma{p}"] = sma
597
+
598
+ result = detect_all_local_patterns(records, closes, highs, lows, volumes, mas, code)
599
+ print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 实时行情查询(多数据源自动切换)。
4
+ 数据源: 腾讯 → 东方财富 → 新浪 → efinance → akshare → tushare → 通达信
5
+ 用法:
6
+ quote.py sh600989 # 单只,表格输出
7
+ quote.py sh600989,sz000807,sh518880 # 批量(≤15/批)
8
+ quote.py @codes.txt # 从文件读代码
9
+ quote.py -j sh600989 # JSON 输出
10
+ quote.py --sources # 显示可用数据源
11
+ """
12
+ import sys
13
+ import json
14
+ from common import split_codes, batchify, normalize_quote_code, parallel_map, err, DataError
15
+ from data import get_quote, get_quotes
16
+
17
+
18
+ def fetch_batch(codes: list, use_cache: bool = True) -> list:
19
+ """批量获取行情,返回 dict 列表(兼容旧接口)。"""
20
+ quotes = get_quotes(codes, use_cache=use_cache)
21
+ return [q.to_dict() for q in quotes]
22
+
23
+
24
+ def main():
25
+ if len(sys.argv) < 2:
26
+ err("用法: quote.py <代码|@文件> [-j] [--sources]")
27
+ args = sys.argv[1:]
28
+
29
+ if "--sources" in args:
30
+ from fetchers import get_quote_fetchers
31
+ fetchers = get_quote_fetchers()
32
+ print("可用行情数据源:")
33
+ for f in fetchers:
34
+ print(f" - {f.name} (优先级 {f.priority})")
35
+ return
36
+
37
+ json_mode = "-j" in args
38
+ args = [a for a in args if a not in ("-j", "--sources")]
39
+
40
+ codes = [normalize_quote_code(c) for c in split_codes(args[0])]
41
+ if not codes:
42
+ err("未提供代码")
43
+
44
+ batches = list(batchify(codes, 15))
45
+ if len(batches) > 1:
46
+ results = parallel_map(lambda b: fetch_batch(b, use_cache=True), batches, max_workers=4, timeout=30)
47
+ all_records = []
48
+ for batch in batches:
49
+ all_records.extend(results.get(batch, []))
50
+ else:
51
+ all_records = fetch_batch(batches[0])
52
+
53
+ if json_mode:
54
+ print(json.dumps(all_records, ensure_ascii=False, indent=2))
55
+ return
56
+
57
+ if not all_records:
58
+ print("(无数据)")
59
+ return
60
+ print(f"{'代码':<10} {'名称':<10} {'现价':>8} {'涨跌%':>7} {'PE':>7} {'换手%':>6} {'市值亿':>8}")
61
+ print("-" * 60)
62
+ for r in all_records:
63
+ print(f"{r['code']:<10} {r['name']:<10} {r['price']:>8} {r['change_pct']:>7} {r['pe']:>7} {r['turnover']:>6} {r['total_cap']:>8}")
64
+
65
+ if __name__ == "__main__":
66
+ try:
67
+ main()
68
+ except DataError as e:
69
+ sys.exit(1)