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,66 @@
|
|
|
1
|
+
"""Yahoo Finance K 线数据源(需要 yfinance 包)。"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
5
|
+
|
|
6
|
+
from common import BaseFetcher
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import yfinance as yf
|
|
10
|
+
HAS_YFINANCE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_YFINANCE = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _to_yf_symbol(code: str) -> str:
|
|
16
|
+
"""转换为 yfinance 符号格式。"""
|
|
17
|
+
plain = code.lstrip("shszSHSZbjBJ").zfill(6)
|
|
18
|
+
if plain.startswith(("60", "68", "51", "56", "58")):
|
|
19
|
+
return f"{plain}.SS"
|
|
20
|
+
elif plain.startswith(("00", "30", "15", "16", "18")):
|
|
21
|
+
return f"{plain}.SZ"
|
|
22
|
+
elif plain.startswith(("43", "83", "87", "88", "92")):
|
|
23
|
+
return f"{plain}.BJ"
|
|
24
|
+
return code
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class YfinanceKlineFetcher(BaseFetcher):
|
|
28
|
+
"""Yahoo Finance K 线数据源 (优先级 4) - 需要安装 yfinance 包。"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__("yfinance_kline", priority=4)
|
|
32
|
+
|
|
33
|
+
def fetch(self, code: str, **kwargs) -> list | None:
|
|
34
|
+
if not HAS_YFINANCE:
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
scale = kwargs.get("scale", 240)
|
|
38
|
+
datalen = kwargs.get("datalen", 30)
|
|
39
|
+
symbol = _to_yf_symbol(code)
|
|
40
|
+
|
|
41
|
+
ticker = yf.Ticker(symbol)
|
|
42
|
+
if scale == 240:
|
|
43
|
+
df = ticker.history(period=f"{datalen}d")
|
|
44
|
+
elif scale == 60:
|
|
45
|
+
df = ticker.history(period="60d", interval="1h")
|
|
46
|
+
elif scale == 5:
|
|
47
|
+
df = ticker.history(period="5d", interval="5m")
|
|
48
|
+
else:
|
|
49
|
+
df = ticker.history(period=f"{datalen}d")
|
|
50
|
+
|
|
51
|
+
if df is None or df.empty:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
result = []
|
|
55
|
+
for idx, row in df.iterrows():
|
|
56
|
+
result.append({
|
|
57
|
+
"day": str(idx)[:10],
|
|
58
|
+
"open": str(round(row.get("Open", 0), 2)),
|
|
59
|
+
"close": str(round(row.get("Close", 0), 2)),
|
|
60
|
+
"high": str(round(row.get("High", 0), 2)),
|
|
61
|
+
"low": str(round(row.get("Low", 0), 2)),
|
|
62
|
+
"volume": str(int(row.get("Volume", 0))),
|
|
63
|
+
})
|
|
64
|
+
return result if result else None
|
|
65
|
+
except Exception:
|
|
66
|
+
return None
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
财务数据查询(多数据源自动切换)。
|
|
4
|
+
数据源: 东方财富 → efinance → akshare
|
|
5
|
+
用法:
|
|
6
|
+
finance.py SH600989 # 单只,最近 4 季
|
|
7
|
+
finance.py -c SH600989,SZ000807 # 批量
|
|
8
|
+
finance.py -j SH600989 # JSON 输出
|
|
9
|
+
finance.py --sources # 显示可用数据源
|
|
10
|
+
"""
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
from common import normalize_finance_code, parallel_map, err, DataError
|
|
14
|
+
from data import get_finance
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fetch(code: str, use_cache: bool = True) -> list:
|
|
18
|
+
"""返回最近 4 季的财务数据(dict 列表,兼容旧接口)。"""
|
|
19
|
+
records = get_finance(code, use_cache=use_cache)
|
|
20
|
+
return [r.to_dict() for r in records]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_table(records: list) -> str:
|
|
24
|
+
if not records:
|
|
25
|
+
return "(无数据)"
|
|
26
|
+
fields = [
|
|
27
|
+
("eps", "每股收益"),
|
|
28
|
+
("roe", "ROE%"),
|
|
29
|
+
("revenue_yoy", "营收同比%"),
|
|
30
|
+
("net_profit_yoy", "净利同比%"),
|
|
31
|
+
("gross_margin", "毛利率%"),
|
|
32
|
+
("net_margin", "净利率%"),
|
|
33
|
+
("debt_ratio", "负债率%"),
|
|
34
|
+
("bps", "每股净资产"),
|
|
35
|
+
("ocf_per_share", "每股现金流"),
|
|
36
|
+
]
|
|
37
|
+
lines = []
|
|
38
|
+
header = " | ".join(["报告期"] + [label for _, label in fields])
|
|
39
|
+
lines.append(header)
|
|
40
|
+
lines.append("-" * len(header))
|
|
41
|
+
for r in records:
|
|
42
|
+
period = r.get("report_date", "?")
|
|
43
|
+
values = [r.get(key, "-") for key, _ in fields]
|
|
44
|
+
lines.append(" | ".join([period] + [str(v)[:8] for v in values]))
|
|
45
|
+
return "\n".join(lines)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
if len(sys.argv) < 2:
|
|
50
|
+
err("用法: finance.py <代码> [-c codes] [-j] [--sources]")
|
|
51
|
+
args = sys.argv[1:]
|
|
52
|
+
|
|
53
|
+
if "--sources" in args:
|
|
54
|
+
from fetchers import get_finance_fetchers
|
|
55
|
+
fetchers = get_finance_fetchers()
|
|
56
|
+
print("可用财务数据源:")
|
|
57
|
+
for f in fetchers:
|
|
58
|
+
print(f" - {f.name} (优先级 {f.priority})")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
json_mode = "-j" in args
|
|
62
|
+
args = [a for a in args if a not in ("-j", "--sources")]
|
|
63
|
+
if not args:
|
|
64
|
+
err("缺少代码")
|
|
65
|
+
|
|
66
|
+
if args[0] == "-c":
|
|
67
|
+
codes = args[1].split(",")
|
|
68
|
+
else:
|
|
69
|
+
codes = [args[0]]
|
|
70
|
+
|
|
71
|
+
normalized_codes = [normalize_finance_code(c) for c in codes]
|
|
72
|
+
|
|
73
|
+
if len(normalized_codes) > 1:
|
|
74
|
+
results = parallel_map(fetch, normalized_codes, max_workers=4, timeout=30)
|
|
75
|
+
all_results = {k: v for k, v in results.items() if v}
|
|
76
|
+
else:
|
|
77
|
+
all_results = {normalized_codes[0]: fetch(normalized_codes[0])}
|
|
78
|
+
|
|
79
|
+
if json_mode:
|
|
80
|
+
print(json.dumps(all_results, ensure_ascii=False, indent=2))
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
for code, records in all_results.items():
|
|
84
|
+
print(f"\n=== {code} ===")
|
|
85
|
+
print(render_table(records))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
try:
|
|
90
|
+
main()
|
|
91
|
+
except DataError as e:
|
|
92
|
+
sys.exit(1)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
首次安装初始化脚本 — 为每个板块拉取前 20 只股票。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python3 scripts/init_pool.py # 检测并初始化(已有数据则跳过)
|
|
7
|
+
python3 scripts/init_pool.py --force # 强制重新初始化
|
|
8
|
+
python3 scripts/init_pool.py --top 30 # 每板块取 Top 30
|
|
9
|
+
|
|
10
|
+
退出码始终为 0,不阻塞安装流程。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
# 复用 refresh_pool 的核心逻辑
|
|
19
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
20
|
+
from refresh_pool import (
|
|
21
|
+
POOL_FILE,
|
|
22
|
+
load_mapping,
|
|
23
|
+
load_current_pool,
|
|
24
|
+
refresh_pool,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# 初始化阈值:低于此值视为未初始化
|
|
28
|
+
MIN_SECTORS = 10
|
|
29
|
+
MIN_STOCKS = 100
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_pool_populated() -> tuple[bool, str]:
|
|
33
|
+
"""检查股票池是否已有足够数据。返回 (是否已初始化, 描述)。"""
|
|
34
|
+
if not os.path.exists(POOL_FILE):
|
|
35
|
+
return False, "股票池文件不存在"
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
with open(POOL_FILE, "r", encoding="utf-8") as f:
|
|
39
|
+
data = json.load(f)
|
|
40
|
+
except (json.JSONDecodeError, OSError):
|
|
41
|
+
return False, "股票池文件损坏"
|
|
42
|
+
|
|
43
|
+
sectors = {k: v for k, v in data.items() if not k.startswith("_")}
|
|
44
|
+
total_stocks = sum(len(v) for v in sectors.values())
|
|
45
|
+
|
|
46
|
+
if len(sectors) < MIN_SECTORS:
|
|
47
|
+
return False, f"仅 {len(sectors)} 个板块(需要 ≥{MIN_SECTORS})"
|
|
48
|
+
if total_stocks < MIN_STOCKS:
|
|
49
|
+
return False, f"仅 {total_stocks} 只股票(需要 ≥{MIN_STOCKS})"
|
|
50
|
+
|
|
51
|
+
return True, f"{len(sectors)} 个板块,{total_stocks} 只股票"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def init_pool(top_n: int = 20, force: bool = False) -> bool:
|
|
55
|
+
"""初始化股票池。返回是否实际执行了初始化。"""
|
|
56
|
+
# 检查是否已初始化
|
|
57
|
+
if not force:
|
|
58
|
+
populated, desc = is_pool_populated()
|
|
59
|
+
if populated:
|
|
60
|
+
print(f"✅ 股票池已存在({desc}),跳过初始化")
|
|
61
|
+
print(" 如需刷新,运行: python3 scripts/refresh_pool.py")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
# 检查 API Token
|
|
65
|
+
token = os.environ.get("EASTMONEY_API_TOKEN", "")
|
|
66
|
+
if not token:
|
|
67
|
+
print("⚠️ 未设置 EASTMONEY_API_TOKEN,无法从东财 API 拉取数据")
|
|
68
|
+
print()
|
|
69
|
+
print("请按以下步骤操作:")
|
|
70
|
+
print(" 1. 设置环境变量: export EASTMONEY_API_TOKEN=你的token")
|
|
71
|
+
print(" (东财 push2 API 的 ut 参数,可从网页版 F12 抓取)")
|
|
72
|
+
print(" 2. 运行初始化: python3 scripts/init_pool.py --force")
|
|
73
|
+
print()
|
|
74
|
+
print("或直接运行: python3 scripts/refresh_pool.py")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
# 执行初始化
|
|
78
|
+
print(f"🚀 初始化股票池(每板块 Top {top_n})...")
|
|
79
|
+
print()
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
new_pool = refresh_pool(top_n=top_n, dry_run=False, show_diff=False)
|
|
83
|
+
total = sum(len(v) for v in new_pool.values())
|
|
84
|
+
print()
|
|
85
|
+
print(f"✅ 初始化完成: {len(new_pool)} 个板块,共 {total} 只股票")
|
|
86
|
+
return True
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"\n❌ 初始化失败: {e}", file=sys.stderr)
|
|
89
|
+
print(" 可稍后重试: python3 scripts/init_pool.py --force")
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main():
|
|
94
|
+
parser = argparse.ArgumentParser(description="首次安装初始化股票池")
|
|
95
|
+
parser.add_argument("--force", "-f", action="store_true",
|
|
96
|
+
help="强制重新初始化(忽略已有数据)")
|
|
97
|
+
parser.add_argument("--top", "-n", type=int, default=20,
|
|
98
|
+
help="每板块取 Top N(默认 20)")
|
|
99
|
+
args = parser.parse_args()
|
|
100
|
+
|
|
101
|
+
init_pool(top_n=args.top, force=args.force)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
package/scripts/kline.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
K 线数据查询(多数据源自动切换)。
|
|
4
|
+
数据源: 新浪 → 东方财富 → 腾讯 → efinance → akshare → tushare → 通达信 → baostock → yfinance
|
|
5
|
+
用法:
|
|
6
|
+
kline.py sh600989 # 日 K,30 根
|
|
7
|
+
kline.py sh600989 5 48 # 5 分钟 K,48 根
|
|
8
|
+
kline.py sh600989 240 30 -j # JSON
|
|
9
|
+
kline.py --sources # 显示可用数据源
|
|
10
|
+
"""
|
|
11
|
+
import sys
|
|
12
|
+
import json
|
|
13
|
+
from common import normalize_quote_code, err, DataError
|
|
14
|
+
from data import get_kline
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fetch(symbol: str, scale: int, datalen: int, use_cache: bool = True) -> list:
|
|
18
|
+
"""获取 K 线数据,返回 dict 列表(兼容旧接口)。"""
|
|
19
|
+
bars = get_kline(symbol, scale, datalen, use_cache=use_cache)
|
|
20
|
+
return [b.to_dict() for b in bars]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_table(records: list) -> str:
|
|
24
|
+
if not records:
|
|
25
|
+
return "(无数据)"
|
|
26
|
+
lines = []
|
|
27
|
+
for d in records:
|
|
28
|
+
lines.append(f"{d['day']} | O:{d['open']:>7} H:{d['high']:>7} L:{d['low']:>7} C:{d['close']:>7} V:{d['volume']:>12}")
|
|
29
|
+
return "\n".join(lines)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
args = sys.argv[1:]
|
|
34
|
+
|
|
35
|
+
if "--sources" in args:
|
|
36
|
+
from fetchers import get_kline_fetchers
|
|
37
|
+
fetchers = get_kline_fetchers()
|
|
38
|
+
print("可用 K 线数据源:")
|
|
39
|
+
for f in fetchers:
|
|
40
|
+
print(f" - {f.name} (优先级 {f.priority})")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
json_mode = "-j" in args
|
|
44
|
+
args = [a for a in args if a not in ("-j", "--sources")]
|
|
45
|
+
if not args:
|
|
46
|
+
err("用法: kline.py <symbol> [scale=240] [datalen=30] [-j]")
|
|
47
|
+
symbol = normalize_quote_code(args[0])
|
|
48
|
+
scale = int(args[1]) if len(args) > 1 else 240
|
|
49
|
+
datalen = int(args[2]) if len(args) > 2 else 30
|
|
50
|
+
|
|
51
|
+
records = fetch(symbol, scale, datalen)
|
|
52
|
+
if json_mode:
|
|
53
|
+
print(json.dumps(records, ensure_ascii=False, indent=2))
|
|
54
|
+
else:
|
|
55
|
+
print(render_table(records))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
try:
|
|
60
|
+
main()
|
|
61
|
+
except DataError as e:
|
|
62
|
+
sys.exit(1)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
数据源健康检查和缓存监控。
|
|
4
|
+
用法:
|
|
5
|
+
python3 scripts/monitor.py # 完整健康检查
|
|
6
|
+
python3 scripts/monitor.py --cache # 缓存状态
|
|
7
|
+
python3 scripts/monitor.py --sources # 数据源状态
|
|
8
|
+
python3 scripts/monitor.py --cleanup # 清理过期缓存
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
18
|
+
from common import CACHE_DIR, cache_cleanup
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_cache_status():
|
|
22
|
+
"""检查缓存目录状态。"""
|
|
23
|
+
if not CACHE_DIR.exists():
|
|
24
|
+
print("缓存目录不存在")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
files = list(CACHE_DIR.glob("*.cache"))
|
|
28
|
+
total_size = sum(f.stat().st_size for f in files)
|
|
29
|
+
expired = sum(1 for f in files if time.time() - f.stat().st_mtime > 21600)
|
|
30
|
+
|
|
31
|
+
print(f"缓存文件数: {len(files)}")
|
|
32
|
+
print(f"缓存总大小: {total_size / 1024:.1f} KB")
|
|
33
|
+
print(f"过期文件数: {expired}")
|
|
34
|
+
|
|
35
|
+
# 按前缀统计
|
|
36
|
+
prefixes = {}
|
|
37
|
+
for f in files:
|
|
38
|
+
prefix = f.name.split("_")[0] if "_" in f.name else "other"
|
|
39
|
+
prefixes[prefix] = prefixes.get(prefix, 0) + 1
|
|
40
|
+
if prefixes:
|
|
41
|
+
print("\n按类型分布:")
|
|
42
|
+
for prefix, count in sorted(prefixes.items()):
|
|
43
|
+
print(f" {prefix}: {count} 个")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_sources():
|
|
47
|
+
"""检查数据源可用性。"""
|
|
48
|
+
from fetchers import get_quote_fetchers, get_kline_fetchers, get_finance_fetchers
|
|
49
|
+
|
|
50
|
+
print("=== 行情数据源 ===")
|
|
51
|
+
for f in get_quote_fetchers():
|
|
52
|
+
status = "✅ 可用" if f.is_available() else "❌ 熔断"
|
|
53
|
+
print(f" {f.name} (优先级 {f.priority}) - {status}")
|
|
54
|
+
|
|
55
|
+
print("\n=== K线数据源 ===")
|
|
56
|
+
for f in get_kline_fetchers():
|
|
57
|
+
status = "✅ 可用" if f.is_available() else "❌ 熔断"
|
|
58
|
+
print(f" {f.name} (优先级 {f.priority}) - {status}")
|
|
59
|
+
|
|
60
|
+
print("\n=== 财务数据源 ===")
|
|
61
|
+
for f in get_finance_fetchers():
|
|
62
|
+
status = "✅ 可用" if f.is_available() else "❌ 熔断"
|
|
63
|
+
print(f" {f.name} (优先级 {f.priority}) - {status}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_health_check():
|
|
67
|
+
"""完整健康检查。"""
|
|
68
|
+
print("🔍 stock-analyzer-skill 健康检查\n")
|
|
69
|
+
|
|
70
|
+
print("--- 缓存状态 ---")
|
|
71
|
+
check_cache_status()
|
|
72
|
+
|
|
73
|
+
print("\n--- 数据源状态 ---")
|
|
74
|
+
check_sources()
|
|
75
|
+
|
|
76
|
+
print("\n--- 行业阈值配置 ---")
|
|
77
|
+
thresholds_path = Path(__file__).resolve().parent / "data" / "industry_thresholds.json"
|
|
78
|
+
if thresholds_path.exists():
|
|
79
|
+
thresholds = json.loads(thresholds_path.read_text(encoding="utf-8"))
|
|
80
|
+
industries = [k for k in thresholds.keys() if not k.startswith("_")]
|
|
81
|
+
print(f" 已配置 {len(industries)} 个行业: {', '.join(industries)}")
|
|
82
|
+
else:
|
|
83
|
+
print(" ⚠ 行业阈值配置文件不存在")
|
|
84
|
+
|
|
85
|
+
print("\n✅ 健康检查完成")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
parser = argparse.ArgumentParser(description="数据源健康检查和缓存监控")
|
|
90
|
+
parser.add_argument("--cache", action="store_true", help="显示缓存状态")
|
|
91
|
+
parser.add_argument("--sources", action="store_true", help="显示数据源状态")
|
|
92
|
+
parser.add_argument("--cleanup", action="store_true", help="清理过期缓存")
|
|
93
|
+
args = parser.parse_args()
|
|
94
|
+
|
|
95
|
+
if args.cache:
|
|
96
|
+
check_cache_status()
|
|
97
|
+
elif args.sources:
|
|
98
|
+
check_sources()
|
|
99
|
+
elif args.cleanup:
|
|
100
|
+
cleaned = cache_cleanup()
|
|
101
|
+
print(f"已清理 {cleaned} 个过期缓存文件")
|
|
102
|
+
else:
|
|
103
|
+
run_health_check()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
main()
|