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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +21 -0
- package/CHANGELOG.md +93 -0
- package/CONTRIBUTING.md +331 -0
- package/README.md +259 -0
- package/experts/README.md +119 -0
- package/experts/buffett.md +91 -0
- package/experts/chaogu_yangjia.md +125 -0
- package/experts/decide.md +212 -0
- package/experts/duan_yongping.md +106 -0
- package/experts/lynch.md +127 -0
- package/experts/soros.md +89 -0
- package/experts/xu_xiang.md +107 -0
- package/experts/zhao_laoge.md +143 -0
- package/experts/zuoshou_xinyi.md +144 -0
- package/install-plugin.js +69 -0
- package/methodology.md +455 -0
- package/package.json +43 -0
- package/scripts/__pycache__/announcements.cpython-314.pyc +0 -0
- package/scripts/__pycache__/backtest.cpython-314.pyc +0 -0
- package/scripts/__pycache__/chan.cpython-314.pyc +0 -0
- package/scripts/__pycache__/classifier.cpython-314.pyc +0 -0
- package/scripts/__pycache__/common.cpython-314.pyc +0 -0
- package/scripts/__pycache__/finance.cpython-314.pyc +0 -0
- package/scripts/__pycache__/init_pool.cpython-314.pyc +0 -0
- package/scripts/__pycache__/kline.cpython-314.pyc +0 -0
- package/scripts/__pycache__/patterns_local.cpython-314.pyc +0 -0
- package/scripts/__pycache__/quote.cpython-314.pyc +0 -0
- package/scripts/__pycache__/refresh_pool.cpython-314.pyc +0 -0
- package/scripts/__pycache__/screener.cpython-314.pyc +0 -0
- package/scripts/__pycache__/technical.cpython-314.pyc +0 -0
- package/scripts/announcements.py +118 -0
- package/scripts/backtest.py +528 -0
- package/scripts/chan.py +591 -0
- package/scripts/classifier.py +302 -0
- package/scripts/common.py +507 -0
- package/scripts/data/__init__.py +208 -0
- package/scripts/data/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/data/__pycache__/cache.cpython-314.pyc +0 -0
- package/scripts/data/__pycache__/config.cpython-314.pyc +0 -0
- package/scripts/data/__pycache__/types.cpython-314.pyc +0 -0
- package/scripts/data/cache.py +99 -0
- package/scripts/data/config.py +49 -0
- package/scripts/data/industry_thresholds.json +199 -0
- package/scripts/data/portfolio_example.json +14 -0
- package/scripts/data/sector_etf.csv +14 -0
- package/scripts/data/sector_mapping.json +64 -0
- package/scripts/data/sector_stocks.json +135 -0
- package/scripts/data/types.py +66 -0
- package/scripts/fetchers/__init__.py +130 -0
- package/scripts/fetchers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/akshare_finance.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/akshare_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/akshare_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/baostock_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_finance.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/eastmoney_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/efinance_finance.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/efinance_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/efinance_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/pytdx_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/sina_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/sina_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tencent_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tencent_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tushare_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/tushare_quote.cpython-314.pyc +0 -0
- package/scripts/fetchers/__pycache__/yfinance_kline.cpython-314.pyc +0 -0
- package/scripts/fetchers/akshare_finance.py +35 -0
- package/scripts/fetchers/akshare_kline.py +59 -0
- package/scripts/fetchers/akshare_quote.py +52 -0
- package/scripts/fetchers/baostock_kline.py +64 -0
- package/scripts/fetchers/eastmoney_finance.py +29 -0
- package/scripts/fetchers/eastmoney_kline.py +48 -0
- package/scripts/fetchers/eastmoney_quote.py +68 -0
- package/scripts/fetchers/efinance_finance.py +32 -0
- package/scripts/fetchers/efinance_kline.py +46 -0
- package/scripts/fetchers/efinance_quote.py +53 -0
- package/scripts/fetchers/pytdx_kline.py +70 -0
- package/scripts/fetchers/pytdx_quote.py +78 -0
- package/scripts/fetchers/sina_kline.py +30 -0
- package/scripts/fetchers/sina_quote.py +35 -0
- package/scripts/fetchers/tencent_kline.py +52 -0
- package/scripts/fetchers/tencent_quote.py +29 -0
- package/scripts/fetchers/tushare_kline.py +62 -0
- package/scripts/fetchers/tushare_quote.py +62 -0
- package/scripts/fetchers/yfinance_kline.py +66 -0
- package/scripts/finance.py +92 -0
- package/scripts/init_pool.py +105 -0
- package/scripts/kline.py +62 -0
- package/scripts/monitor.py +107 -0
- package/scripts/patterns_local.py +599 -0
- package/scripts/quote.py +69 -0
- package/scripts/refresh_pool.py +328 -0
- package/scripts/screener.py +434 -0
- package/scripts/strategies/__init__.py +11 -0
- package/scripts/strategies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/strategies/__pycache__/registry.cpython-314.pyc +0 -0
- package/scripts/strategies/__pycache__/thresholds.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__init__.py +8 -0
- package/scripts/strategies/factors/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/liquidity.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/momentum.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/quality.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/valuation.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/__pycache__/volatility.cpython-314.pyc +0 -0
- package/scripts/strategies/factors/liquidity.py +49 -0
- package/scripts/strategies/factors/momentum.py +45 -0
- package/scripts/strategies/factors/quality.py +54 -0
- package/scripts/strategies/factors/valuation.py +76 -0
- package/scripts/strategies/factors/volatility.py +89 -0
- package/scripts/strategies/registry.py +87 -0
- package/scripts/strategies/thresholds.py +28 -0
- package/scripts/technical/__init__.py +116 -0
- package/scripts/technical/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/astock.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/boll.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/candlestick.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/core.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/kdj.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/macd.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/moving_average.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/report.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/rsi.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/scoring.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/signals.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/trend.cpython-314.pyc +0 -0
- package/scripts/technical/__pycache__/volume.cpython-314.pyc +0 -0
- package/scripts/technical/astock.py +98 -0
- package/scripts/technical/boll.py +49 -0
- package/scripts/technical/candlestick.py +151 -0
- package/scripts/technical/core.py +92 -0
- package/scripts/technical/kdj.py +68 -0
- package/scripts/technical/macd.py +97 -0
- package/scripts/technical/moving_average.py +59 -0
- package/scripts/technical/report.py +221 -0
- package/scripts/technical/rsi.py +37 -0
- package/scripts/technical/scoring.py +392 -0
- package/scripts/technical/signals.py +70 -0
- package/scripts/technical/trend.py +143 -0
- package/scripts/technical/volume.py +113 -0
- package/scripts/technical.py +215 -0
- package/skills/financial-analyst/SKILL.md +141 -0
- package/skills/help/SKILL.md +188 -0
- package/skills/init/SKILL.md +66 -0
- package/skills/investment-researcher/SKILL.md +152 -0
- package/skills/market/SKILL.md +99 -0
- package/skills/portfolio/SKILL.md +96 -0
- package/skills/screener/SKILL.md +128 -0
- package/skills/sector/SKILL.md +102 -0
- package/skills/stock/SKILL.md +148 -0
- package/skills/technical/SKILL.md +168 -0
- package/workflow.md +91 -0
package/scripts/chan.py
ADDED
|
@@ -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))
|