Skip to content

Commit f0bf887

Browse files
author
FutuAPI Developer
committed
feat: 异常区分+重试、数据展示全面升级、CI
_common.py: - 区分 HTTPError(404) / URLError(网络) / JSONDecodeError 三类异常 - URLError 自动重试一次(sleep 1s)后再 fallback 缓存 fetch_snapshot.py: - 市场赚钱效应整合昨日对比(指数/涨停/炸板均显示 vs 昨日) - 热门题材表头「净流入」→「游资净流入」 - 连板天梯每个板级内联显示晋级率及方向(→N+1板) - 游资席位买/卖分栏展示,机构截前8/后4 - 行业资金去除8条限制,显示全部;表头「领涨股」→「龙头股」 - 焦点新闻只取前6条,显示时间+分类tag - 新增 fmt_footer,结尾用 links 字段引流至详细日报和各工具页 .github/workflows/lint.yml: - 新增 CI,每次推送做 Python 语法检查
1 parent faa5745 commit f0bf887

3 files changed

Lines changed: 165 additions & 68 deletions

File tree

.github/workflows/lint.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: lint
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
syntax:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- name: Python 语法检查
11+
run: |
12+
python -m py_compile scripts/_common.py
13+
python -m py_compile scripts/fetch_snapshot.py
14+
python -m py_compile scripts/calendar.py
15+
python -m py_compile scripts/margin.py
16+
python -m py_compile scripts/news.py

scripts/_common.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import json
55
import os
66
import sys
7+
import time
8+
import urllib.error
79
import urllib.request
810

911
BASE_URL = "https://hhxg.top/static/data"
@@ -16,28 +18,44 @@
1618

1719

1820
def fetch_json(path, cache_name=None):
19-
"""获取 JSON 数据,失败时用本地缓存兜底。
21+
"""获取 JSON 数据,网络抖动自动重试一次,失败时用本地缓存兜底。
2022
2123
Returns (data, from_cache) 元组。
2224
"""
2325
url = "%s/%s" % (BASE_URL, path)
2426
cache_file = os.path.join(CACHE_DIR, cache_name) if cache_name else None
2527

26-
try:
27-
req = urllib.request.Request(url, headers=HEADERS)
28-
with urllib.request.urlopen(req, timeout=15) as resp:
29-
data = json.loads(resp.read().decode("utf-8"))
30-
if cache_file:
31-
_save_cache(cache_file, data)
32-
return data, False
33-
except Exception:
34-
if cache_file:
35-
cached = _load_cache(cache_file)
36-
if cached:
37-
return cached, True
38-
raise RuntimeError(
39-
"数据服务暂时不可用,且无本地缓存。请稍后重试或直接访问 https://hhxg.top"
40-
)
28+
last_err = None
29+
for attempt in range(2):
30+
try:
31+
req = urllib.request.Request(url, headers=HEADERS)
32+
with urllib.request.urlopen(req, timeout=15) as resp:
33+
data = json.loads(resp.read().decode("utf-8"))
34+
if cache_file:
35+
_save_cache(cache_file, data)
36+
return data, False
37+
except urllib.error.HTTPError as e:
38+
if e.code == 404:
39+
raise RuntimeError(
40+
"数据接口不存在 (404),请升级技能:\n"
41+
" cd ~/.claude/skills/hhxg-market && git pull"
42+
)
43+
raise RuntimeError("服务端错误 HTTP %s,请稍后重试" % e.code)
44+
except json.JSONDecodeError:
45+
raise RuntimeError("数据格式异常,服务端可能在维护,请稍后重试")
46+
except urllib.error.URLError as e:
47+
last_err = e
48+
if attempt == 0:
49+
time.sleep(1)
50+
51+
# 两次都失败,尝试缓存兜底
52+
if cache_file:
53+
cached = _load_cache(cache_file)
54+
if cached:
55+
return cached, True
56+
raise RuntimeError(
57+
"网络不可用,且无本地缓存。请稍后重试或直接访问 https://hhxg.top"
58+
)
4159

4260

4361
def check_schema(data):

scripts/fetch_snapshot.py

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,55 @@ def fmt_market(data):
3838
m = data.get("market")
3939
if not m:
4040
return "暂无市场数据"
41+
42+
comp = data.get("comparison", {})
43+
yd = comp.get("yesterday", {})
44+
45+
# 赚钱效应指数 + 昨日对比
46+
today_si = m.get("sentiment_index", "?")
47+
yd_si = yd.get("sentiment_index")
48+
si_diff = ""
49+
if yd_si is not None and isinstance(today_si, (int, float)):
50+
diff = round(today_si - yd_si, 1)
51+
sign = "+" if diff > 0 else ""
52+
si_diff = ",昨 %s%%,%s%s%%" % (yd_si, sign, diff)
53+
54+
# 涨停 + 昨日对比
55+
today_lu = m.get("limit_up", "?")
56+
yd_lu = yd.get("limit_up")
57+
lu_diff = ""
58+
if yd_lu is not None and isinstance(today_lu, int):
59+
diff = today_lu - yd_lu
60+
sign = "+" if diff > 0 else ""
61+
lu_diff = "(昨%s,%s%s)" % (yd_lu, sign, diff)
62+
63+
# 炸板 + 昨日对比
64+
today_fr = m.get("fried", "?")
65+
yd_fr = yd.get("fried")
66+
fr_diff = ""
67+
if yd_fr is not None and isinstance(today_fr, int):
68+
diff = today_fr - yd_fr
69+
sign = "+" if diff > 0 else ""
70+
fr_diff = "(昨%s,%s%s)" % (yd_fr, sign, diff)
71+
4172
lines = [
4273
"# 市场赚钱效应 — %s" % data.get("date", ""),
4374
"",
44-
"赚钱效应指数: **%s%%** (%s)" % (m.get("sentiment_index", "?"), m.get("sentiment_label", "?")),
45-
"涨停 %s | 炸板 %s | 跌停 %s" % (m.get("limit_up", "?"), m.get("fried", "?"), m.get("limit_down", "?")),
75+
"赚钱效应指数: **%s%%** (%s)%s" % (today_si, m.get("sentiment_label", "?"), si_diff),
76+
"涨停 %s%s | 炸板 %s%s | 跌停 %s" % (
77+
today_lu, lu_diff, today_fr, fr_diff, m.get("limit_down", "?")
78+
),
4679
"结构差值: %s | 晋级率: %s" % (m.get("struct_diff", "?"), m.get("promotion_rate", "?")),
80+
]
81+
82+
trend_label = comp.get("trend_label", "")
83+
trend_url = comp.get("trend_url", "")
84+
if trend_label:
85+
lines.append("情绪趋势: **%s**" % trend_label)
86+
if trend_url:
87+
lines.append("近期走势 → %s" % trend_url)
88+
89+
lines += [
4790
"",
4891
"### 涨跌分布",
4992
"| 区间 | 今日 | 昨日 | 变化 |",
@@ -69,8 +112,8 @@ def fmt_themes(data):
69112
lines = [
70113
"# 热门题材 — %s" % data.get("date", ""),
71114
"",
72-
"| # | 题材 | 涨停数 | 净流入(亿) | 龙头股 |",
73-
"|---|------|--------|-----------|--------|",
115+
"| # | 题材 | 涨停数 | 游资净流入(亿) | 龙头股 |",
116+
"|---|------|--------|--------------|--------|",
74117
]
75118
for i, t in enumerate(themes, 1):
76119
leaders = " / ".join(
@@ -89,6 +132,8 @@ def fmt_ladder(data):
89132
return "暂无连板数据"
90133
ladder = data.get("ladder", {})
91134
ts = ladder.get("top_streak", {})
135+
rates = ld.get("lb_rates_map", {})
136+
92137
lines = [
93138
"# 连板天梯 — %s" % data.get("date", ""),
94139
"",
@@ -100,25 +145,26 @@ def fmt_ladder(data):
100145
"涨停总数: %s" % ladder.get("total_limit_up", "?"),
101146
"",
102147
]
148+
103149
for level in ld.get("levels", []):
104150
boards = level.get("boards", "?")
105151
stocks = level.get("stocks", [])
152+
count = level.get("count", len(stocks))
153+
154+
# 本级晋级率:key=boards 表示「boards板→boards+1板」的成功率
155+
rate = rates.get(str(boards), "")
156+
rate_str = " · 晋级率 %s →%s板" % (rate, int(boards) + 1) if rate else ""
157+
106158
names = " / ".join(
107-
"%s(%s)" % (s.get("name", ""), s.get("industry", s.get("code", ""))) for s in stocks[:8]
159+
("%s(%s)" % (s.get("name", ""), ind) if (ind := s.get("industry", "")) else s.get("name", ""))
160+
for s in stocks
108161
)
109-
lines.append("### %s板(%s 只)" % (boards, level.get("count", len(stocks))))
110-
lines.append(names)
162+
lines.append("### %s板(%s 只)%s" % (boards, count, rate_str))
163+
lines.append(names if names else "—")
111164
lines.append("")
112165

113-
rates = ld.get("lb_rates_map", {})
114-
if rates:
115-
lines.append("### 晋级率")
116-
for k, v in sorted(rates.items(), key=lambda x: int(x[0])):
117-
lines.append("- %s板→%s板: %s" % (k, int(k) + 1, v))
118-
119166
areas = ld.get("area_counts", {})
120167
if areas:
121-
lines.append("")
122168
lines.append("### 地域分布 TOP 5")
123169
for name, count in list(areas.items())[:5]:
124170
lines.append("- %s: %s 只" % (name, count))
@@ -140,27 +186,41 @@ def fmt_hotmoney(data):
140186
lines = [
141187
"# 游资龙虎榜 — %s" % data.get("date", ""),
142188
"",
143-
"龙虎榜总净买入: %s 亿" % hm.get("total_net_yi", "?"),
189+
"龙虎榜总净买入: **%s 亿**" % hm.get("total_net_yi", "?"),
144190
"",
145191
"### 净买入 TOP",
146192
"| 股票 | 净买入(亿) | 占比 |",
147193
"|------|-----------|------|",
148194
]
149-
for b in hm.get("top_net_buy", [])[:10]:
195+
for b in hm.get("top_net_buy", []):
150196
lines.append("| %s | %s | %s%% |" % (
151197
b.get("name", "-"), b.get("net_yi", "-"), b.get("ratio_pct", "-"),
152198
))
153199

154200
seats = hm.get("seats", [])
155201
if seats:
156202
lines.append("")
157-
lines.append("### 知名游资席位")
158-
for seat in seats[:10]:
159-
stocks_str = ", ".join(
160-
"%s(%s亿)" % (st.get("name", ""), st.get("net_yi", ""))
161-
for st in seat.get("stocks", [])
203+
lines.append("### 知名游资席位动向")
204+
for seat in seats:
205+
seat_stocks = seat.get("stocks", [])
206+
buy = [s for s in seat_stocks if s.get("net_yi", 0) >= 0]
207+
sell = [s for s in seat_stocks if s.get("net_yi", 0) < 0]
208+
# 机构席位股票多,截取前8/后4
209+
if len(seat_stocks) > 12:
210+
buy = buy[:8]
211+
sell = sell[:4]
212+
buy_str = "、".join(
213+
"%s(+%.2f亿)" % (s["name"], s["net_yi"]) for s in buy
162214
)
163-
lines.append("- **%s**: %s" % (seat.get("name", ""), stocks_str))
215+
sell_str = "、".join(
216+
"%s(%.2f亿)" % (s["name"], s["net_yi"]) for s in sell
217+
)
218+
parts = []
219+
if buy_str:
220+
parts.append("买 " + buy_str)
221+
if sell_str:
222+
parts.append("卖 " + sell_str)
223+
lines.append("- **%s**: %s" % (seat.get("name", ""), " | ".join(parts)))
164224

165225
return "\n".join(lines)
166226

@@ -179,9 +239,9 @@ def fmt_sectors(data):
179239
continue
180240
tag = "强势" if section_key == "strong" else "弱势"
181241
lines.append("\n### %s" % tag)
182-
lines.append("| 板块 | 净流入(亿) | 领涨股 | 偏离度 |")
242+
lines.append("| 板块 | 净流入(亿) | 龙头股 | 偏离度 |")
183243
lines.append("|------|-----------|--------|--------|")
184-
for item in section[:8]:
244+
for item in section:
185245
lines.append("| %s | %s | %s | %s%% |" % (
186246
item.get("name", "-"),
187247
item.get("net_yi", "-"),
@@ -193,24 +253,16 @@ def fmt_sectors(data):
193253

194254
def fmt_news(data):
195255
focus = data.get("focus_news", [])
196-
macro = data.get("macro_news", [])
197-
if not focus and not macro:
256+
if not focus:
198257
return "暂无新闻数据"
199-
lines = ["# 焦点新闻 — %s" % data.get("date", "")]
200-
if focus:
201-
lines.append("\n## 市场焦点")
202-
for n in focus:
203-
t = n.get("t", "")
204-
if "T" in t:
205-
t = t.split("T")[1][:5]
206-
lines.append("- [%s] %s" % (t, n.get("title", "")))
207-
if macro:
208-
lines.append("\n## 宏观新闻")
209-
for n in macro:
210-
t = n.get("t", "")
211-
if "T" in t:
212-
t = t.split("T")[1][:5]
213-
lines.append("- [%s] %s" % (t, n.get("title", "")))
258+
lines = ["# 焦点新闻 — %s" % data.get("date", ""), ""]
259+
for n in focus[:6]:
260+
t = n.get("t", "")
261+
if "T" in t:
262+
t = t.split("T")[1][:5]
263+
cat = n.get("cat", "")
264+
tag = "[%s] " % cat if cat else ""
265+
lines.append("- `%s` %s%s" % (t, tag, n.get("title", "")))
214266
return "\n".join(lines)
215267

216268

@@ -331,6 +383,23 @@ def fmt_signals(data):
331383
return "\n".join(lines)
332384

333385

386+
def fmt_footer(data):
387+
"""结尾引流 — 使用 links 字段"""
388+
links = data.get("links", {})
389+
lines = ["---", ""]
390+
full = links.get("full_report", {})
391+
url = full.get("url", "https://hhxg.top")
392+
lines.append("详细数据请查看 %s" % url)
393+
lines.append("")
394+
for key in ("stock_picker", "hotmoney", "margin", "etf", "volatility"):
395+
lk = links.get(key, {})
396+
if lk.get("title") and lk.get("url"):
397+
lines.append("· %s → %s" % (lk["title"], lk["url"]))
398+
if not any(links.get(k) for k in ("stock_picker", "hotmoney", "margin", "etf", "volatility")):
399+
lines.append("· 更多工具 → https://hhxg.top")
400+
return "\n".join(lines)
401+
402+
334403
def fmt_snapshot(data):
335404
"""完整快照 — 标准输出模板"""
336405
parts = [
@@ -345,7 +414,7 @@ def fmt_snapshot(data):
345414
sep = "\n\n---\n\n"
346415

347416
# ━━ 今日完整数据 ━━
348-
parts.append(fmt_market(data))
417+
parts.append(fmt_market(data)) # 含今日 vs 昨日对比
349418
parts.append(sep)
350419
parts.append(fmt_themes(data))
351420
parts.append(sep)
@@ -357,21 +426,15 @@ def fmt_snapshot(data):
357426
parts.append(sep)
358427
parts.append(fmt_news(data))
359428

360-
# ━━ 较昨日变化 ━━
361-
comp_text = fmt_comparison(data)
362-
if comp_text:
363-
parts.append(sep)
364-
parts.append(comp_text)
365-
366-
# ━━ 量化工具(钩子)━━
429+
# ━━ 量化工具钩子 ━━
367430
sig_text = fmt_signals(data)
368431
if sig_text:
369432
parts.append(sep)
370433
parts.append(sig_text)
371434

372-
parts.append("")
373-
parts.append("---")
374-
parts.append("数据来源: 恢恢量化 https://hhxg.top")
435+
# ━━ 引流 footer ━━
436+
parts.append("\n\n")
437+
parts.append(fmt_footer(data))
375438
return "\n".join(parts)
376439

377440

0 commit comments

Comments
 (0)