aggroot 1.0.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/.env.example +173 -0
- package/README.md +43 -0
- package/dist/cli.cjs +22 -0
- package/dist/index.cjs +43931 -0
- package/dist/tree-sitter-c.wasm +0 -0
- package/dist/tree-sitter-cpp.wasm +0 -0
- package/dist/web-tree-sitter.wasm +0 -0
- package/install-linux.sh +107 -0
- package/install-windows.bat +103 -0
- package/package.json +151 -0
- package/share/agents/agent-ai-partner.json +66 -0
- package/share/agents/agent-ai.json +48 -0
- package/share/agents/agent-alpha-investor.json +97 -0
- package/share/agents/agent-coder.json +211 -0
- package/share/agents/agent-devops.json +59 -0
- package/share/agents/agent-resume.json +117 -0
- package/share/agents/agent-risk-control.json +84 -0
- package/share/agents/agent-writer.json +213 -0
- package/share/scripts/_utils.py +105 -0
- package/share/scripts/market_overview.py +244 -0
- package/share/scripts/stock_compare.py +244 -0
- package/share/scripts/stock_financial.py +211 -0
- package/share/scripts/stock_news.py +181 -0
- package/share/scripts/stock_quote.py +433 -0
- package/share/scripts/stock_technical.py +402 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
股票技术指标分析
|
|
3
|
+
|
|
4
|
+
用法: python stock_technical.py <stock_code> [--indicator <type>] [--period <days>]
|
|
5
|
+
stock_code: 6位A股代码(如 600519) 或 5位港股代码(如 03888)
|
|
6
|
+
--indicator: 指标类型 ma/macd/rsi/kdj/boll/all,默认all
|
|
7
|
+
--period: 历史天数,默认120
|
|
8
|
+
|
|
9
|
+
AKShare接口:
|
|
10
|
+
主: ak.stock_zh_a_daily(symbol) — 新浪日线行情(稳定可用,代码需带sh/sz前缀)
|
|
11
|
+
备: ak.stock_zh_a_hist(symbol) — 东方财富日线行情(当前不稳定)
|
|
12
|
+
港股主: ak.stock_hk_daily(symbol) — 新浪港股日线(快速)
|
|
13
|
+
港股备: ak.stock_hk_hist(symbol) — 东方财富港股日线
|
|
14
|
+
|
|
15
|
+
本地计算: MA(5/10/20/60), MACD(12,26,9), RSI(6/12/24), KDJ(9,3,3), BOLL(20,2)
|
|
16
|
+
|
|
17
|
+
注意: 新浪接口代码需带市场前缀(如 sh600519),频繁调用会被封IP
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import argparse
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, '.')
|
|
25
|
+
from _utils import normalize_code, get_market_prefix, output_json, output_error, check_akshare, safe_float, is_hk_code
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def calc_ma(closes, periods=[5, 10, 20, 60]):
|
|
29
|
+
"""计算移动平均线"""
|
|
30
|
+
result = {}
|
|
31
|
+
for p in periods:
|
|
32
|
+
if len(closes) >= p:
|
|
33
|
+
result[f"ma{p}"] = round(sum(closes[-p:]) / p, 2)
|
|
34
|
+
else:
|
|
35
|
+
result[f"ma{p}"] = None
|
|
36
|
+
|
|
37
|
+
# 判断排列
|
|
38
|
+
vals = [result.get(f"ma{p}") for p in periods if result.get(f"ma{p}") is not None]
|
|
39
|
+
if len(vals) >= 2:
|
|
40
|
+
if all(vals[i] >= vals[i+1] for i in range(len(vals)-1)):
|
|
41
|
+
result["signal"] = "多头排列"
|
|
42
|
+
elif all(vals[i] <= vals[i+1] for i in range(len(vals)-1)):
|
|
43
|
+
result["signal"] = "空头排列"
|
|
44
|
+
else:
|
|
45
|
+
result["signal"] = "交叉排列"
|
|
46
|
+
else:
|
|
47
|
+
result["signal"] = "数据不足"
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def calc_macd(closes, fast=12, slow=26, signal=9):
|
|
52
|
+
"""计算MACD"""
|
|
53
|
+
if len(closes) < slow + signal:
|
|
54
|
+
return {"dif": None, "dea": None, "macd_hist": None, "signal": "数据不足"}
|
|
55
|
+
|
|
56
|
+
# EMA计算
|
|
57
|
+
def ema(data, period):
|
|
58
|
+
k = 2 / (period + 1)
|
|
59
|
+
result = data[0]
|
|
60
|
+
for val in data[1:]:
|
|
61
|
+
result = val * k + result * (1 - k)
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
# 计算DIF序列
|
|
65
|
+
dif_list = []
|
|
66
|
+
for i in range(slow - 1, len(closes)):
|
|
67
|
+
fast_ema = ema(closes[i-fast+1:i+1] if i >= fast-1 else closes[:i+1], fast)
|
|
68
|
+
slow_ema = ema(closes[i-slow+1:i+1], slow)
|
|
69
|
+
dif_list.append(fast_ema - slow_ema)
|
|
70
|
+
|
|
71
|
+
if len(dif_list) < signal:
|
|
72
|
+
return {"dif": None, "dea": None, "macd_hist": None, "signal": "数据不足"}
|
|
73
|
+
|
|
74
|
+
dea = ema(dif_list, signal)
|
|
75
|
+
dif = dif_list[-1]
|
|
76
|
+
hist = 2 * (dif - dea)
|
|
77
|
+
|
|
78
|
+
# 信号判断
|
|
79
|
+
prev_hist = 2 * (dif_list[-2] - ema(dif_list[:-1], signal)) if len(dif_list) >= 2 else 0
|
|
80
|
+
if hist > 0 and prev_hist <= 0:
|
|
81
|
+
sig = "金叉"
|
|
82
|
+
elif hist < 0 and prev_hist >= 0:
|
|
83
|
+
sig = "死叉"
|
|
84
|
+
elif hist > 0:
|
|
85
|
+
sig = "多头"
|
|
86
|
+
else:
|
|
87
|
+
sig = "空头"
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"dif": round(dif, 3),
|
|
91
|
+
"dea": round(dea, 3),
|
|
92
|
+
"macd_hist": round(hist, 3),
|
|
93
|
+
"signal": sig,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def calc_rsi(closes, periods=[6, 12, 24]):
|
|
98
|
+
"""计算RSI"""
|
|
99
|
+
result = {}
|
|
100
|
+
for p in periods:
|
|
101
|
+
if len(closes) < p + 1:
|
|
102
|
+
result[f"rsi{p}"] = None
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
gains = []
|
|
106
|
+
losses = []
|
|
107
|
+
for i in range(-p, 0):
|
|
108
|
+
diff = closes[i] - closes[i-1]
|
|
109
|
+
gains.append(max(diff, 0))
|
|
110
|
+
losses.append(max(-diff, 0))
|
|
111
|
+
|
|
112
|
+
avg_gain = sum(gains) / p
|
|
113
|
+
avg_loss = sum(losses) / p
|
|
114
|
+
|
|
115
|
+
if avg_loss == 0:
|
|
116
|
+
rsi = 100
|
|
117
|
+
else:
|
|
118
|
+
rs = avg_gain / avg_loss
|
|
119
|
+
rsi = 100 - (100 / (1 + rs))
|
|
120
|
+
|
|
121
|
+
result[f"rsi{p}"] = round(rsi, 2)
|
|
122
|
+
|
|
123
|
+
# 信号判断
|
|
124
|
+
rsi6 = result.get("rsi6")
|
|
125
|
+
if rsi6 is not None:
|
|
126
|
+
if rsi6 > 80:
|
|
127
|
+
result["signal"] = "超买"
|
|
128
|
+
elif rsi6 > 60:
|
|
129
|
+
result["signal"] = "偏强"
|
|
130
|
+
elif rsi6 > 40:
|
|
131
|
+
result["signal"] = "中性"
|
|
132
|
+
elif rsi6 > 20:
|
|
133
|
+
result["signal"] = "偏弱"
|
|
134
|
+
else:
|
|
135
|
+
result["signal"] = "超卖"
|
|
136
|
+
else:
|
|
137
|
+
result["signal"] = "数据不足"
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def calc_kdj(highs, lows, closes, n=9, m1=3, m2=3):
|
|
143
|
+
"""计算KDJ"""
|
|
144
|
+
if len(closes) < n:
|
|
145
|
+
return {"k": None, "d": None, "j": None, "signal": "数据不足"}
|
|
146
|
+
|
|
147
|
+
# 取最近n天
|
|
148
|
+
recent_highs = highs[-n:]
|
|
149
|
+
recent_lows = lows[-n:]
|
|
150
|
+
close = closes[-1]
|
|
151
|
+
|
|
152
|
+
hn = max(recent_highs)
|
|
153
|
+
ln = min(recent_lows)
|
|
154
|
+
|
|
155
|
+
if hn == ln:
|
|
156
|
+
rsv = 50
|
|
157
|
+
else:
|
|
158
|
+
rsv = (close - ln) / (hn - ln) * 100
|
|
159
|
+
|
|
160
|
+
# 简化计算(使用前一天的K/D值默认50)
|
|
161
|
+
k = (2 / m1) * 50 + (1 / m1) * rsv
|
|
162
|
+
d = (2 / m2) * 50 + (1 / m2) * k
|
|
163
|
+
j = 3 * k - 2 * d
|
|
164
|
+
|
|
165
|
+
# 信号判断
|
|
166
|
+
if j > 100:
|
|
167
|
+
sig = "超买"
|
|
168
|
+
elif k > d and k < 80:
|
|
169
|
+
sig = "偏强"
|
|
170
|
+
elif k < d and k > 20:
|
|
171
|
+
sig = "偏弱"
|
|
172
|
+
elif j < 0:
|
|
173
|
+
sig = "超卖"
|
|
174
|
+
else:
|
|
175
|
+
sig = "中性"
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"k": round(k, 2),
|
|
179
|
+
"d": round(d, 2),
|
|
180
|
+
"j": round(j, 2),
|
|
181
|
+
"signal": sig,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def calc_boll(closes, n=20, k=2):
|
|
186
|
+
"""计算布林带"""
|
|
187
|
+
if len(closes) < n:
|
|
188
|
+
return {"upper": None, "middle": None, "lower": None, "signal": "数据不足"}
|
|
189
|
+
|
|
190
|
+
recent = closes[-n:]
|
|
191
|
+
middle = sum(recent) / n
|
|
192
|
+
std = (sum((x - middle) ** 2 for x in recent) / n) ** 0.5
|
|
193
|
+
upper = middle + k * std
|
|
194
|
+
lower = middle - k * std
|
|
195
|
+
current = closes[-1]
|
|
196
|
+
|
|
197
|
+
# 位置判断
|
|
198
|
+
if current > upper:
|
|
199
|
+
pos = "上轨上方"
|
|
200
|
+
sig = "超买"
|
|
201
|
+
elif current > middle:
|
|
202
|
+
pos = "中轨上方"
|
|
203
|
+
sig = "中性偏强"
|
|
204
|
+
elif current > lower:
|
|
205
|
+
pos = "中轨下方"
|
|
206
|
+
sig = "中性偏弱"
|
|
207
|
+
else:
|
|
208
|
+
pos = "下轨下方"
|
|
209
|
+
sig = "超卖"
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
"upper": round(upper, 2),
|
|
213
|
+
"middle": round(middle, 2),
|
|
214
|
+
"lower": round(lower, 2),
|
|
215
|
+
"current": round(current, 2),
|
|
216
|
+
"current_position": pos,
|
|
217
|
+
"signal": sig,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main():
|
|
222
|
+
parser = argparse.ArgumentParser(description='股票技术指标分析')
|
|
223
|
+
parser.add_argument('stock_code', help='股票代码')
|
|
224
|
+
parser.add_argument('--indicator', default='all',
|
|
225
|
+
choices=['ma', 'macd', 'rsi', 'kdj', 'boll', 'all'],
|
|
226
|
+
help='指标类型(默认all)')
|
|
227
|
+
parser.add_argument('--period', type=int, default=120, help='历史天数(默认120)')
|
|
228
|
+
args = parser.parse_args()
|
|
229
|
+
|
|
230
|
+
err = check_akshare()
|
|
231
|
+
if err:
|
|
232
|
+
output_error(err)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
code = normalize_code(args.stock_code)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
import akshare as ak
|
|
239
|
+
|
|
240
|
+
# 计算日期范围
|
|
241
|
+
end_date = datetime.now().strftime('%Y%m%d')
|
|
242
|
+
start_date = (datetime.now() - timedelta(days=args.period * 2)).strftime('%Y%m%d')
|
|
243
|
+
|
|
244
|
+
df = None
|
|
245
|
+
closes = []
|
|
246
|
+
highs = []
|
|
247
|
+
lows = []
|
|
248
|
+
latest_date = ''
|
|
249
|
+
|
|
250
|
+
# 港股:优先分钟线合成当日K线+新浪daily,失败回退东方财富hist
|
|
251
|
+
if is_hk_code(code):
|
|
252
|
+
# 1. 取新浪日线(T-1,稳定快速)
|
|
253
|
+
daily_df = None
|
|
254
|
+
try:
|
|
255
|
+
daily_df = ak.stock_hk_daily(symbol=code, adjust="qfq")
|
|
256
|
+
if not daily_df.empty:
|
|
257
|
+
daily_df = daily_df.tail(args.period * 2)
|
|
258
|
+
except Exception:
|
|
259
|
+
daily_df = None
|
|
260
|
+
|
|
261
|
+
# 2. 尝试分钟线合成当日K线(盘中实时)
|
|
262
|
+
intraday_appended = False
|
|
263
|
+
try:
|
|
264
|
+
min_df = ak.stock_hk_hist_min_em(symbol=code, period='1')
|
|
265
|
+
if not min_df.empty:
|
|
266
|
+
today_str = str(min_df.iloc[-1].get('时间', ''))[:10]
|
|
267
|
+
today_data = min_df[min_df['时间'].astype(str).str.startswith(today_str)]
|
|
268
|
+
if not today_data.empty and daily_df is not None:
|
|
269
|
+
last_daily_date = str(daily_df.iloc[-1].get('date', ''))
|
|
270
|
+
if today_str > last_daily_date:
|
|
271
|
+
# 当日数据比日线更新,合成当日K线追加
|
|
272
|
+
today_close = safe_float(today_data.iloc[-1].get('收盘'))
|
|
273
|
+
today_high = max(safe_float(r.get('最高')) for _, r in today_data.iterrows() if safe_float(r.get('最高')) is not None)
|
|
274
|
+
today_low = min(safe_float(r.get('最低')) for _, r in today_data.iterrows() if safe_float(r.get('最低')) is not None)
|
|
275
|
+
today_open = safe_float(today_data.iloc[0].get('开盘'))
|
|
276
|
+
if today_close is not None:
|
|
277
|
+
daily_df = daily_df._append({
|
|
278
|
+
'date': today_str, 'open': today_open,
|
|
279
|
+
'high': today_high, 'low': today_low,
|
|
280
|
+
'close': today_close, 'volume': 0, 'amount': 0,
|
|
281
|
+
}, ignore_index=True)
|
|
282
|
+
intraday_appended = True
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
if daily_df is not None and not daily_df.empty:
|
|
287
|
+
closes = [safe_float(x) for x in daily_df['close'].tolist() if safe_float(x) is not None]
|
|
288
|
+
highs = [safe_float(x) for x in daily_df['high'].tolist() if safe_float(x) is not None]
|
|
289
|
+
lows = [safe_float(x) for x in daily_df['low'].tolist() if safe_float(x) is not None]
|
|
290
|
+
latest_date = str(daily_df.iloc[-1].get('date', ''))
|
|
291
|
+
df = daily_df # 标记成功
|
|
292
|
+
|
|
293
|
+
# 3. 兜底:东方财富港股日线
|
|
294
|
+
if df is None or df.empty:
|
|
295
|
+
try:
|
|
296
|
+
df = ak.stock_hk_hist(
|
|
297
|
+
symbol=code,
|
|
298
|
+
period="daily",
|
|
299
|
+
start_date=start_date,
|
|
300
|
+
end_date=end_date,
|
|
301
|
+
adjust="qfq"
|
|
302
|
+
)
|
|
303
|
+
if not df.empty:
|
|
304
|
+
closes = [safe_float(x) for x in df['收盘'].tolist() if safe_float(x) is not None]
|
|
305
|
+
highs = [safe_float(x) for x in df['最高'].tolist() if safe_float(x) is not None]
|
|
306
|
+
lows = [safe_float(x) for x in df['最低'].tolist() if safe_float(x) is not None]
|
|
307
|
+
latest_date = str(df.iloc[-1].get('日期', ''))
|
|
308
|
+
except Exception:
|
|
309
|
+
df = None
|
|
310
|
+
|
|
311
|
+
if df is None or df.empty:
|
|
312
|
+
output_error(f"未找到港股 {code} 的历史数据,港股代码为5位数字(如 03888)")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
else:
|
|
316
|
+
# A股:优先新浪(稳定),失败回退东方财富
|
|
317
|
+
try:
|
|
318
|
+
prefix = get_market_prefix(code)
|
|
319
|
+
sina_code = f"{prefix}{code}"
|
|
320
|
+
df = ak.stock_zh_a_daily(
|
|
321
|
+
symbol=sina_code,
|
|
322
|
+
start_date=start_date,
|
|
323
|
+
end_date=end_date,
|
|
324
|
+
adjust="qfq"
|
|
325
|
+
)
|
|
326
|
+
if not df.empty:
|
|
327
|
+
# 新浪列名: date/open/high/low/close/volume/amount
|
|
328
|
+
closes = [safe_float(x) for x in df['close'].tolist() if safe_float(x) is not None]
|
|
329
|
+
highs = [safe_float(x) for x in df['high'].tolist() if safe_float(x) is not None]
|
|
330
|
+
lows = [safe_float(x) for x in df['low'].tolist() if safe_float(x) is not None]
|
|
331
|
+
latest_date = str(df.iloc[-1].get('date', ''))
|
|
332
|
+
except Exception:
|
|
333
|
+
df = None
|
|
334
|
+
|
|
335
|
+
# 东方财富 fallback
|
|
336
|
+
if df is None or df.empty:
|
|
337
|
+
try:
|
|
338
|
+
df = ak.stock_zh_a_hist(
|
|
339
|
+
symbol=code,
|
|
340
|
+
period="daily",
|
|
341
|
+
start_date=start_date,
|
|
342
|
+
end_date=end_date,
|
|
343
|
+
adjust="qfq"
|
|
344
|
+
)
|
|
345
|
+
if not df.empty:
|
|
346
|
+
closes = [safe_float(x) for x in df['收盘'].tolist() if safe_float(x) is not None]
|
|
347
|
+
highs = [safe_float(x) for x in df['最高'].tolist() if safe_float(x) is not None]
|
|
348
|
+
lows = [safe_float(x) for x in df['最低'].tolist() if safe_float(x) is not None]
|
|
349
|
+
latest_date = str(df.iloc[-1].get('日期', ''))
|
|
350
|
+
except Exception:
|
|
351
|
+
df = None
|
|
352
|
+
|
|
353
|
+
if df is None or df.empty:
|
|
354
|
+
output_error(f"未找到股票 {code} 的历史数据")
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
indicators = {}
|
|
358
|
+
indicator = args.indicator
|
|
359
|
+
|
|
360
|
+
if indicator in ('ma', 'all'):
|
|
361
|
+
indicators["ma"] = calc_ma(closes)
|
|
362
|
+
if indicator in ('macd', 'all'):
|
|
363
|
+
indicators["macd"] = calc_macd(closes)
|
|
364
|
+
if indicator in ('rsi', 'all'):
|
|
365
|
+
indicators["rsi"] = calc_rsi(closes)
|
|
366
|
+
if indicator in ('kdj', 'all'):
|
|
367
|
+
indicators["kdj"] = calc_kdj(highs, lows, closes)
|
|
368
|
+
if indicator in ('boll', 'all'):
|
|
369
|
+
indicators["boll"] = calc_boll(closes)
|
|
370
|
+
|
|
371
|
+
data = {
|
|
372
|
+
"code": code,
|
|
373
|
+
"latest_date": latest_date,
|
|
374
|
+
"data_points": len(closes),
|
|
375
|
+
"data_time": latest_date + (" (盘中实时)" if intraday_appended else " (收盘)"),
|
|
376
|
+
"indicators": indicators,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# 时效性检查
|
|
380
|
+
if latest_date:
|
|
381
|
+
try:
|
|
382
|
+
from datetime import date as date_cls
|
|
383
|
+
today = date_cls.today()
|
|
384
|
+
data_date = date_cls.fromisoformat(latest_date[:10])
|
|
385
|
+
days_old = (today - data_date).days
|
|
386
|
+
if days_old > 0:
|
|
387
|
+
data["data_staleness"] = f"最新K线为{days_old}天前({latest_date})"
|
|
388
|
+
if not intraday_appended:
|
|
389
|
+
data["data_staleness"] += ",日线数据为T-1(最近交易日收盘)"
|
|
390
|
+
if days_old > 3:
|
|
391
|
+
data["data_staleness"] += ",数据可能过时,请确认是否为非交易日"
|
|
392
|
+
except (ValueError, TypeError):
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
output_json(data)
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
output_error(f"获取技术指标失败: {str(e)}")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == '__main__':
|
|
402
|
+
main()
|