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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
K 线形态识别。
|
|
3
|
+
依赖: core (to_float via common)
|
|
4
|
+
"""
|
|
5
|
+
from common import to_float
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect_candle_patterns(records):
|
|
9
|
+
"""识别最近 4 根 K 线的形态。"""
|
|
10
|
+
if len(records) < 4:
|
|
11
|
+
return []
|
|
12
|
+
|
|
13
|
+
patterns = []
|
|
14
|
+
bars = []
|
|
15
|
+
for r in records[-4:]:
|
|
16
|
+
bars.append({
|
|
17
|
+
"open": to_float(r.get("open")),
|
|
18
|
+
"high": to_float(r.get("high")),
|
|
19
|
+
"low": to_float(r.get("low")),
|
|
20
|
+
"close": to_float(r.get("close")),
|
|
21
|
+
"date": r.get("day", ""),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
# 单根形态(最近3根)
|
|
25
|
+
for i, b in enumerate(bars[-3:]):
|
|
26
|
+
idx = len(bars) - 3 + i
|
|
27
|
+
prev_close = bars[idx - 1]["close"] if idx > 0 else b["open"]
|
|
28
|
+
singles = _candle_single(b, prev_close)
|
|
29
|
+
for s in singles:
|
|
30
|
+
patterns.append({"date": b["date"], "type": s, "position": f"T-{len(bars)-1-idx}"})
|
|
31
|
+
|
|
32
|
+
# A 股特化形态
|
|
33
|
+
if len(bars) >= 2:
|
|
34
|
+
ah = _candle_ashare(bars[-2], bars[-1])
|
|
35
|
+
if ah:
|
|
36
|
+
patterns.append({"date": bars[-1]["date"], "type": ah, "position": "T-0"})
|
|
37
|
+
|
|
38
|
+
# 双根组合
|
|
39
|
+
if len(bars) >= 2:
|
|
40
|
+
doubles = _candle_double(bars[-2], bars[-1])
|
|
41
|
+
for d in doubles:
|
|
42
|
+
patterns.append({"date": bars[-1]["date"], "type": d, "position": "T-1~0"})
|
|
43
|
+
|
|
44
|
+
# 三根组合
|
|
45
|
+
if len(bars) >= 3:
|
|
46
|
+
triples = _candle_triple(bars[-3], bars[-2], bars[-1])
|
|
47
|
+
for t in triples:
|
|
48
|
+
patterns.append({"date": bars[-1]["date"], "type": t, "position": "T-2~0"})
|
|
49
|
+
|
|
50
|
+
return patterns
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _body_shadow(bar):
|
|
54
|
+
"""计算实体、上影线、下影线。"""
|
|
55
|
+
body = abs(bar["close"] - bar["open"])
|
|
56
|
+
upper_shadow = bar["high"] - max(bar["close"], bar["open"])
|
|
57
|
+
lower_shadow = min(bar["close"], bar["open"]) - bar["low"]
|
|
58
|
+
total = bar["high"] - bar["low"]
|
|
59
|
+
return body, upper_shadow, lower_shadow, total
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_bullish(bar):
|
|
63
|
+
return bar["close"] > bar["open"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _candle_single(bar, prev_close):
|
|
67
|
+
"""单根 K 线形态识别。"""
|
|
68
|
+
body, upper, lower, total = _body_shadow(bar)
|
|
69
|
+
if total <= 0:
|
|
70
|
+
return []
|
|
71
|
+
patterns = []
|
|
72
|
+
|
|
73
|
+
# 十字星
|
|
74
|
+
if body / total < 0.1:
|
|
75
|
+
patterns.append("十字星(变盘信号)")
|
|
76
|
+
# 锤子线
|
|
77
|
+
elif lower > 2 * body and (bar["high"] - bar["close"]) < body:
|
|
78
|
+
patterns.append("锤子线(底部反转)")
|
|
79
|
+
# 倒锤子
|
|
80
|
+
elif upper > 2 * body and (bar["close"] - bar["low"]) < body:
|
|
81
|
+
patterns.append("倒锤子(可能见顶)")
|
|
82
|
+
# 光头光脚阳线
|
|
83
|
+
elif _is_bullish(bar) and bar["close"] == bar["high"] and bar["open"] == bar["low"]:
|
|
84
|
+
patterns.append("光头光脚阳线(强势)")
|
|
85
|
+
# 光头光脚阴线
|
|
86
|
+
elif not _is_bullish(bar) and bar["open"] == bar["high"] and bar["close"] == bar["low"]:
|
|
87
|
+
patterns.append("光头光脚阴线(弱势)")
|
|
88
|
+
# T 字线
|
|
89
|
+
elif body / total < 0.05 and upper < body and lower > 3 * body:
|
|
90
|
+
patterns.append("T字线(下方支撑强)")
|
|
91
|
+
# 倒 T 字
|
|
92
|
+
elif body / total < 0.05 and lower < body and upper > 3 * body:
|
|
93
|
+
patterns.append("倒T字(上方压力大)")
|
|
94
|
+
|
|
95
|
+
return patterns
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _candle_ashare(prev, curr):
|
|
99
|
+
"""A 股特化形态。"""
|
|
100
|
+
if _is_bullish(curr):
|
|
101
|
+
if curr["close"] < prev["close"]:
|
|
102
|
+
return "假阳真阴(收阳但实际下跌)"
|
|
103
|
+
else:
|
|
104
|
+
if curr["close"] > prev["close"]:
|
|
105
|
+
return "假阴真阳(收阴但实际上涨)"
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _candle_double(b1, b2):
|
|
110
|
+
"""双根 K 线组合。"""
|
|
111
|
+
patterns = []
|
|
112
|
+
# 阳包阴
|
|
113
|
+
if not _is_bullish(b1) and _is_bullish(b2) and b2["close"] > b1["open"] and b2["open"] < b1["close"]:
|
|
114
|
+
patterns.append("阳包阴(看涨吞没)")
|
|
115
|
+
# 阴包阳
|
|
116
|
+
if _is_bullish(b1) and not _is_bullish(b2) and b2["close"] < b1["open"] and b2["open"] > b1["close"]:
|
|
117
|
+
patterns.append("阴包阳(看跌吞没)")
|
|
118
|
+
# 平底/平顶
|
|
119
|
+
if abs(b1["low"] - b2["low"]) / max(b1["low"], 0.01) < 0.005:
|
|
120
|
+
patterns.append("平底(支撑确认)")
|
|
121
|
+
if abs(b1["high"] - b2["high"]) / max(b1["high"], 0.01) < 0.005:
|
|
122
|
+
patterns.append("平顶(压力确认)")
|
|
123
|
+
# 揉搓线:一长上影一长下影
|
|
124
|
+
body1, upper1, lower1, _ = _body_shadow(b1)
|
|
125
|
+
body2, upper2, lower2, _ = _body_shadow(b2)
|
|
126
|
+
if (upper1 > 2 * body1 and lower2 > 2 * body2) or (lower1 > 2 * body1 and upper2 > 2 * body2):
|
|
127
|
+
patterns.append("揉搓线(洗盘或变盘)")
|
|
128
|
+
return patterns
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _candle_triple(b1, b2, b3):
|
|
132
|
+
"""三根 K 线组合。"""
|
|
133
|
+
patterns = []
|
|
134
|
+
body1, _, _, _ = _body_shadow(b1)
|
|
135
|
+
body2, _, _, _ = _body_shadow(b2)
|
|
136
|
+
body3, _, _, _ = _body_shadow(b3)
|
|
137
|
+
|
|
138
|
+
# 早晨之星:阴线 + 小实体 + 阳线
|
|
139
|
+
if not _is_bullish(b1) and body2 < body1 * 0.5 and _is_bullish(b3) and b3["close"] > (b1["open"] + b1["close"]) / 2:
|
|
140
|
+
patterns.append("早晨之星(底部反转)")
|
|
141
|
+
# 黄昏之星:阳线 + 小实体 + 阴线
|
|
142
|
+
if _is_bullish(b1) and body2 < body1 * 0.5 and not _is_bullish(b3) and b3["close"] < (b1["open"] + b1["close"]) / 2:
|
|
143
|
+
patterns.append("黄昏之星(顶部反转)")
|
|
144
|
+
# 红三兵
|
|
145
|
+
if _is_bullish(b1) and _is_bullish(b2) and _is_bullish(b3) and b1["close"] < b2["close"] < b3["close"]:
|
|
146
|
+
if b2["open"] > b1["open"] and b3["open"] > b2["open"]:
|
|
147
|
+
patterns.append("红三兵(强势延续)")
|
|
148
|
+
# 三只乌鸦
|
|
149
|
+
if not _is_bullish(b1) and not _is_bullish(b2) and not _is_bullish(b3) and b1["close"] > b2["close"] > b3["close"]:
|
|
150
|
+
patterns.append("三只乌鸦(弱势延续)")
|
|
151
|
+
return patterns
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
核心数学工具和数据解析。
|
|
3
|
+
无内部依赖,仅使用标准库。
|
|
4
|
+
"""
|
|
5
|
+
import math
|
|
6
|
+
import statistics
|
|
7
|
+
|
|
8
|
+
from common import to_float
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ═══════════════════════════════════════════════════════════════
|
|
12
|
+
# 数学工具
|
|
13
|
+
# ═══════════════════════════════════════════════════════════════
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def sma(values, period):
|
|
17
|
+
"""简单移动平均。"""
|
|
18
|
+
if len(values) < period:
|
|
19
|
+
return statistics.mean(values) if values else 0
|
|
20
|
+
return statistics.mean(values[-period:])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ema(values, period):
|
|
24
|
+
"""指数移动平均。"""
|
|
25
|
+
if len(values) < period:
|
|
26
|
+
return statistics.mean(values) if values else 0
|
|
27
|
+
k = 2 / (period + 1)
|
|
28
|
+
result = statistics.mean(values[:period])
|
|
29
|
+
for v in values[period:]:
|
|
30
|
+
result = v * k + result * (1 - k)
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ema_series(values, period):
|
|
35
|
+
"""返回 EMA 序列(用于背离检测和 KDJ)。"""
|
|
36
|
+
if len(values) < period:
|
|
37
|
+
return []
|
|
38
|
+
k = 2 / (period + 1)
|
|
39
|
+
result = [statistics.mean(values[:period])]
|
|
40
|
+
for v in values[period:]:
|
|
41
|
+
result.append(v * k + result[-1] * (1 - k))
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def stddev(values):
|
|
46
|
+
"""总体标准差。"""
|
|
47
|
+
if len(values) < 2:
|
|
48
|
+
return 0
|
|
49
|
+
mean = statistics.mean(values)
|
|
50
|
+
return math.sqrt(sum((x - mean) ** 2 for x in values) / len(values))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _find_swing_points(values, window=5):
|
|
54
|
+
"""找局部极值点索引列表。用于背离检测。"""
|
|
55
|
+
if len(values) < 2 * window + 1:
|
|
56
|
+
return [], []
|
|
57
|
+
highs, lows = [], []
|
|
58
|
+
for i in range(window, len(values) - window):
|
|
59
|
+
left = values[i - window:i]
|
|
60
|
+
right = values[i + 1:i + window + 1]
|
|
61
|
+
if values[i] >= max(left) and values[i] > max(right):
|
|
62
|
+
highs.append(i)
|
|
63
|
+
if values[i] <= min(left) and values[i] < min(right):
|
|
64
|
+
lows.append(i)
|
|
65
|
+
return highs, lows
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ═══════════════════════════════════════════════════════════════
|
|
69
|
+
# 数据解析
|
|
70
|
+
# ═══════════════════════════════════════════════════════════════
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_records(records):
|
|
74
|
+
"""将 K 线数据转成数值列表(统一过滤零值,保持数组对齐)。"""
|
|
75
|
+
# 先过滤掉任一字段为 0 的整条记录,确保所有数组索引对齐
|
|
76
|
+
valid_records = []
|
|
77
|
+
for r in records:
|
|
78
|
+
c = to_float(r.get("close"))
|
|
79
|
+
o = to_float(r.get("open"))
|
|
80
|
+
h = to_float(r.get("high"))
|
|
81
|
+
lo = to_float(r.get("low"))
|
|
82
|
+
v = to_float(r.get("volume"))
|
|
83
|
+
if c > 0 and o > 0 and h > 0 and lo > 0 and v > 0:
|
|
84
|
+
valid_records.append(r)
|
|
85
|
+
|
|
86
|
+
closes = [to_float(r.get("close")) for r in valid_records]
|
|
87
|
+
opens = [to_float(r.get("open")) for r in valid_records]
|
|
88
|
+
highs = [to_float(r.get("high")) for r in valid_records]
|
|
89
|
+
lows = [to_float(r.get("low")) for r in valid_records]
|
|
90
|
+
volumes = [to_float(r.get("volume")) for r in valid_records]
|
|
91
|
+
|
|
92
|
+
return closes, opens, highs, lows, volumes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KDJ 指标(含钝化检测)。
|
|
3
|
+
无内部依赖。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def kdj_full(closes, highs, lows, n=9):
|
|
8
|
+
"""KDJ 指标 + 钝化检测。"""
|
|
9
|
+
if len(closes) < n + 1:
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
# 计算 KDJ 序列
|
|
13
|
+
k_series, d_series = [], []
|
|
14
|
+
k_val, d_val = 50, 50
|
|
15
|
+
for i in range(n - 1, len(closes)):
|
|
16
|
+
low_n = min(lows[i - n + 1:i + 1])
|
|
17
|
+
high_n = max(highs[i - n + 1:i + 1])
|
|
18
|
+
rsv = ((closes[i] - low_n) / (high_n - low_n) * 100) if high_n != low_n else 50
|
|
19
|
+
k_val = 2 / 3 * k_val + 1 / 3 * rsv
|
|
20
|
+
d_val = 2 / 3 * d_val + 1 / 3 * k_val
|
|
21
|
+
k_series.append(k_val)
|
|
22
|
+
d_series.append(d_val)
|
|
23
|
+
|
|
24
|
+
k_now = k_series[-1]
|
|
25
|
+
d_now = d_series[-1]
|
|
26
|
+
j_now = 3 * k_now - 2 * d_now
|
|
27
|
+
|
|
28
|
+
# 金叉死叉
|
|
29
|
+
kdj_signal = "正常"
|
|
30
|
+
if len(k_series) >= 2:
|
|
31
|
+
if k_series[-2] <= d_series[-2] and k_now > d_now:
|
|
32
|
+
kdj_signal = "金叉"
|
|
33
|
+
elif k_series[-2] >= d_series[-2] and k_now < d_now:
|
|
34
|
+
kdj_signal = "死叉"
|
|
35
|
+
|
|
36
|
+
# 超买超卖区(附加信息,不覆盖金叉/死叉)
|
|
37
|
+
zone = ""
|
|
38
|
+
if j_now > 100:
|
|
39
|
+
zone = f"超买区(J={j_now:.0f})"
|
|
40
|
+
elif j_now < 0:
|
|
41
|
+
zone = f"超卖区(J={j_now:.0f})"
|
|
42
|
+
|
|
43
|
+
# 组合信号:金叉+超卖 或 死叉+超买
|
|
44
|
+
if zone:
|
|
45
|
+
if kdj_signal == "金叉" and j_now < 0:
|
|
46
|
+
kdj_signal = "金叉+超卖"
|
|
47
|
+
elif kdj_signal == "死叉" and j_now > 100:
|
|
48
|
+
kdj_signal = "死叉+超买"
|
|
49
|
+
elif kdj_signal == "正常":
|
|
50
|
+
kdj_signal = zone
|
|
51
|
+
|
|
52
|
+
# A 股特化:KDJ 钝化检测
|
|
53
|
+
dunhua = False
|
|
54
|
+
if len(k_series) >= 5:
|
|
55
|
+
if all(k > 80 for k in k_series[-5:]):
|
|
56
|
+
dunhua = True
|
|
57
|
+
kdj_signal += " [KDJ高位钝化-趋势延续]"
|
|
58
|
+
elif all(k < 20 for k in k_series[-5:]):
|
|
59
|
+
dunhua = True
|
|
60
|
+
kdj_signal += " [KDJ低位钝化-趋势延续]"
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"k": round(k_now, 2),
|
|
64
|
+
"d": round(d_now, 2),
|
|
65
|
+
"j": round(j_now, 2),
|
|
66
|
+
"signal": kdj_signal,
|
|
67
|
+
"钝化": dunhua,
|
|
68
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MACD 指标(含背离检测)。
|
|
3
|
+
依赖: core (ema, _ema_series, _find_swing_points)
|
|
4
|
+
"""
|
|
5
|
+
from .core import ema, _ema_series, _find_swing_points
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def macd_full(closes):
|
|
9
|
+
"""MACD 完整分析:DIF/DEA/柱/金叉死叉 + 顶背离/底背离。"""
|
|
10
|
+
if len(closes) < 34:
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
# DIF/DEA
|
|
14
|
+
ema12 = ema(closes, 12)
|
|
15
|
+
ema26 = ema(closes, 26)
|
|
16
|
+
dif = ema12 - ema26
|
|
17
|
+
|
|
18
|
+
# 计算 DIF 序列
|
|
19
|
+
ema12_series = _ema_series(closes, 12)
|
|
20
|
+
ema26_series = _ema_series(closes, 26)
|
|
21
|
+
min_len = min(len(ema12_series), len(ema26_series))
|
|
22
|
+
dif_series = [ema12_series[i] - ema26_series[i] for i in range(min_len)]
|
|
23
|
+
|
|
24
|
+
dea_series = _ema_series(dif_series, 9)
|
|
25
|
+
dea = dea_series[-1] if dea_series else dif
|
|
26
|
+
prev_dif = dif_series[-2] if len(dif_series) >= 2 else dif
|
|
27
|
+
prev_dea = dea_series[-2] if len(dea_series) >= 2 else dea
|
|
28
|
+
macd_bar = (dif - dea) * 2
|
|
29
|
+
|
|
30
|
+
# 金叉死叉检测
|
|
31
|
+
signal = 0
|
|
32
|
+
if prev_dif <= prev_dea and dif > dea:
|
|
33
|
+
signal = 1
|
|
34
|
+
elif prev_dif >= prev_dea and dif < dea:
|
|
35
|
+
signal = -1
|
|
36
|
+
|
|
37
|
+
# 柱状图趋势
|
|
38
|
+
prev_bar = (prev_dif - prev_dea) * 2
|
|
39
|
+
if macd_bar > 0 and macd_bar > prev_bar:
|
|
40
|
+
bar_trend = "红柱放大"
|
|
41
|
+
elif macd_bar > 0 and macd_bar <= prev_bar:
|
|
42
|
+
bar_trend = "红柱缩小"
|
|
43
|
+
elif macd_bar < 0 and macd_bar < prev_bar:
|
|
44
|
+
bar_trend = "绿柱放大"
|
|
45
|
+
else:
|
|
46
|
+
bar_trend = "绿柱缩小"
|
|
47
|
+
|
|
48
|
+
# 背离检测
|
|
49
|
+
divergence = _detect_macd_divergence(closes, dif_series, dea_series)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"dif": round(dif, 4),
|
|
53
|
+
"dea": round(dea, 4),
|
|
54
|
+
"macd_bar": round(macd_bar, 4),
|
|
55
|
+
"signal": signal,
|
|
56
|
+
"signal_desc": {1: "金叉", -1: "死叉", 0: "无"}.get(signal),
|
|
57
|
+
"bar_trend": bar_trend,
|
|
58
|
+
"divergence": divergence,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _detect_macd_divergence(closes, dif_series, dea_series):
|
|
63
|
+
"""检测 MACD 顶背离/底背离。"""
|
|
64
|
+
if len(closes) < 60 or len(dif_series) < 60:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
lookback = min(60, len(closes))
|
|
68
|
+
c = closes[-lookback:]
|
|
69
|
+
d = dif_series[-lookback:]
|
|
70
|
+
|
|
71
|
+
price_highs, price_lows = _find_swing_points(c, window=5)
|
|
72
|
+
dif_highs, dif_lows = _find_swing_points(d, window=5)
|
|
73
|
+
|
|
74
|
+
# 顶背离:价格新高而 DIF 未新高
|
|
75
|
+
if len(price_highs) >= 2:
|
|
76
|
+
last2_p = sorted(price_highs[-2:])
|
|
77
|
+
if last2_p[1] - last2_p[0] >= 8:
|
|
78
|
+
if c[last2_p[1]] > c[last2_p[0]]:
|
|
79
|
+
# 找到对应的 DIF 峰值
|
|
80
|
+
relevant_dif_peaks = [i for i in dif_highs if abs(i - last2_p[0]) <= 5 or abs(i - last2_p[1]) <= 5]
|
|
81
|
+
if len(relevant_dif_peaks) >= 2:
|
|
82
|
+
relevant_dif_peaks.sort()
|
|
83
|
+
if d[relevant_dif_peaks[-1]] < d[relevant_dif_peaks[0]]:
|
|
84
|
+
return "顶背离(看跌)"
|
|
85
|
+
|
|
86
|
+
# 底背离:价格新低而 DIF 未新低
|
|
87
|
+
if len(price_lows) >= 2:
|
|
88
|
+
last2_p = sorted(price_lows[-2:])
|
|
89
|
+
if last2_p[1] - last2_p[0] >= 8:
|
|
90
|
+
if c[last2_p[1]] < c[last2_p[0]]:
|
|
91
|
+
relevant_dif_lows = [i for i in dif_lows if abs(i - last2_p[0]) <= 5 or abs(i - last2_p[1]) <= 5]
|
|
92
|
+
if len(relevant_dif_lows) >= 2:
|
|
93
|
+
relevant_dif_lows.sort()
|
|
94
|
+
if d[relevant_dif_lows[-1]] > d[relevant_dif_lows[0]]:
|
|
95
|
+
return "底背离(看涨)"
|
|
96
|
+
|
|
97
|
+
return None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
均线系统分析。
|
|
3
|
+
依赖: core (sma, stddev)
|
|
4
|
+
"""
|
|
5
|
+
import statistics
|
|
6
|
+
|
|
7
|
+
from .core import sma, stddev
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_MA_PERIODS = [5, 10, 20, 60, 120, 250]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ma_system(closes):
|
|
14
|
+
"""均线系统分析。返回 MA 值、排列状态、粘合度、支撑/阻力均线。"""
|
|
15
|
+
result = {}
|
|
16
|
+
for p in _MA_PERIODS:
|
|
17
|
+
result[f"ma{p}"] = round(sma(closes, p), 2) if len(closes) >= p else None
|
|
18
|
+
|
|
19
|
+
# 排列状态
|
|
20
|
+
mas = [result[f"ma{p}"] for p in _MA_PERIODS if result[f"ma{p}"] is not None]
|
|
21
|
+
if len(mas) >= 4:
|
|
22
|
+
if all(mas[i] > mas[i+1] for i in range(len(mas)-1) if mas[i] and mas[i+1]):
|
|
23
|
+
result["alignment"] = "多头排列"
|
|
24
|
+
elif all(mas[i] < mas[i+1] for i in range(len(mas)-1) if mas[i] and mas[i+1]):
|
|
25
|
+
result["alignment"] = "空头排列"
|
|
26
|
+
else:
|
|
27
|
+
result["alignment"] = "交叉震荡"
|
|
28
|
+
else:
|
|
29
|
+
result["alignment"] = "数据不足"
|
|
30
|
+
|
|
31
|
+
# MA 粘合度 (MA5/10/20)
|
|
32
|
+
short_mas = [result.get(f"ma{p}") for p in [5, 10, 20] if result.get(f"ma{p}")]
|
|
33
|
+
if len(short_mas) >= 3 and statistics.mean(short_mas) > 0:
|
|
34
|
+
conv = stddev(short_mas) / statistics.mean(short_mas)
|
|
35
|
+
result["convergence"] = round(conv, 4)
|
|
36
|
+
if conv < 0.02:
|
|
37
|
+
result["convergence_desc"] = "高度粘合(变盘窗口)"
|
|
38
|
+
elif conv < 0.05:
|
|
39
|
+
result["convergence_desc"] = "中度粘合"
|
|
40
|
+
else:
|
|
41
|
+
result["convergence_desc"] = "发散"
|
|
42
|
+
else:
|
|
43
|
+
result["convergence"] = None
|
|
44
|
+
result["convergence_desc"] = "数据不足"
|
|
45
|
+
|
|
46
|
+
# 支撑/阻力均线(相对当前价格)
|
|
47
|
+
last = closes[-1] if closes else 0
|
|
48
|
+
supports, resistances = [], []
|
|
49
|
+
for p in _MA_PERIODS:
|
|
50
|
+
v = result.get(f"ma{p}")
|
|
51
|
+
if v is None:
|
|
52
|
+
continue
|
|
53
|
+
if v < last:
|
|
54
|
+
supports.append((f"MA{p}", v))
|
|
55
|
+
else:
|
|
56
|
+
resistances.append((f"MA{p}", v))
|
|
57
|
+
result["ma_supports"] = sorted(supports, key=lambda x: x[1], reverse=True)
|
|
58
|
+
result["ma_resistances"] = sorted(resistances, key=lambda x: x[1])
|
|
59
|
+
return result
|