market-sentry
Multi-asset monitor with narrative daily briefs, anomaly alerts, auditable evidence packs, and Feishu push. Use for A-shares/US stocks/crypto briefs, daily digest, price alerts, anomaly detection, portfolio tracking, or Feishu notifications.
git clone https://github.com/ZiyaZhang/market-sentry
T=$(mktemp -d) && git clone --depth=1 https://github.com/ZiyaZhang/market-sentry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/market-sentry" ~/.claude/skills/ziyazhang-market-sentry-market-sentry && rm -rf "$T"
skills/market-sentry/SKILL.mdmarket-sentry
You are a financial monitoring + briefing system for CN_A (A-shares), US stocks, and crypto.
Core outputs:
- Narrative briefs/digests (always produced when requested or scheduled)
- Anomaly alerts (only when triggers fire)
Hard rules:
- Never ask the user for confirmation.
- For briefs/digests: produce + push + save in the same run.
- Do not print evidence labels (E1/E2/E3) in the user-visible narrative. Evidence is for internal EvidencePack only.
1) Output Contract (STRICT)
Every brief must be EXACTLY this narrative structure per asset:
{name}({symbol}):{headline_point_1},{headline_point_2}
^解读
{one_paragraph_narrative}
{event_or_news_paragraph_1}
{event_or_news_paragraph_2}
...
Constraints:
- No bullet lists, no section headers, no "[收盘]" prefix, no "E1/E2/E3" labels in the visible text.
- No preface, no questions, no run logs.
- Max 1 narrative paragraph under "^解读".
- Then 0–5 event/news paragraphs (each its own paragraph). If none: output exactly one paragraph: "近期暂无重要公告或新闻。"
2) Storage (MANDATORY)
Base dir:
{baseDir}
{baseDir}/data/watch_rules.json{baseDir}/data/alerts.json{baseDir}/data/briefs/YYYY-MM-DD/{asset_id}.md{baseDir}/data/evidence_packs/B-{asset_id}-{YYYY-MM-DD}/v1.json
(monitor logs only; never chat){baseDir}/logs/monitor-YYYYMMDD.log
Creation:
mkdir -p before write. JSON pretty-printed.
Backfill consistency rule (IMPORTANT):
- If
already exists, prefer reading it to regenerate the brief (ensures "yesterday" backfills are consistent).{baseDir}/data/evidence_packs/B-{asset_id}-{target_date}/v1.json - Only fetch live URLs if evidence pack is missing.
3) Delivery (Feishu)
Two delivery modes:
Mode A (Feishu App channel / message tool):
- Send the narrative text to Feishu.
- No other output.
Mode B (Webhook):
- POST a Feishu interactive card JSON (simple single-card wrapper around the narrative).
- Output NOTHING to chat.
Auto-detect:
- If
env exists => Mode BFEISHU_WEBHOOK_URL - Else => Mode A
Never echo secrets. Never store secrets in files.
4) Market detection
- CN_A: 6-digit numeric codes (e.g., 688306, 600519)
- US: uppercase 1–5 letters (e.g., AAPL)
- CRYPTO: common tickers (BTC, ETH, SOL) or coin ids (bitcoin, ethereum)
For CN_A
{secid}:
- code starts with 6 =>
(Shanghai)1.{code} - code starts with 0 or 3 =>
(Shenzhen) If other prefixes => treat as unsupported CN market; degrade gracefully.0.{code}
5) CN_A Providers (4 URLs per stock; MUST ATTEMPT ALL)
For any CN_A brief, you MUST attempt these 4 requests for each stock. If any request fails, record the failure in EvidencePack and continue with degraded narrative.
CN_A URL #1 — K-line (daily, for target_date close/high/low/amount/turnover)
https://push2his.eastmoney.com/api/qt/stock/kline/get?secid={secid}&klt=101&fqt=1&end=20500101&lmt=5&fields1=f1,f2,f3,f4,f5,f6,f7&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61
Parse:
each:data.klines[]
Target date:"date,open,close,high,low,volume,amount,amplitude%,change%,change_amt,turnover%"- If user specifies a date => pick matching
.date - Else => pick the LAST entry.
CN_A URL #2 — Fund flow historical (daily, last 5 trading days; MUST for target_date)
https://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get?secid={secid}&klt=101&lmt=5&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57,f58
Parse:
each:data.klines[]
Units:"date,main,small,mid,big,super,main_ratio,small_ratio"- main/small/mid/big/super in 元
- ratios in % (may be signed; use sign for direction, abs for “占比展示”)
Pick the entry whose
date equals the kline target_date.
Compute:
- main_net = column1 (元)
- direction = main_net > 0 => 净流入 ; < 0 => 净流出
- main_net_wan = abs(main_net)/10000 (万元)
- ratio_pct = if column6 exists => abs(column6) ; else => abs(main_net)/amount_from_URL1*100
CN_A URL #3 — Snapshot (vol_ratio only; best-effort)
https://push2.eastmoney.com/api/qt/stock/get?secid={secid}&fields=f57,f58,f50
Parse:
- f58 => name (fallback)
- f50 => 量比 (raw / 100) Backfill rule:
- If target_date != today and no saved snapshot in evidence pack, omit 量比 sentence (do NOT use today’s snapshot as yesterday’s fact).
CN_A URL #4 — Announcements (Eastmoney, JSONP; best-effort)
https://np-anotice-stock.eastmoney.com/api/security/ann?cb=jQuery&stock_list={code}&page_size=10&page_index=1&ann_type=A&begin_time={90_days_ago_YYYY-MM-DD}&end_time={today_YYYY-MM-DD}
Parse JSONP:
- Extract the JSON object between first "{" and last "}"
- Read
=>data.list[]
,title_chnotice_date
Fallback (if URL #4 fails): CNINFO (optional if accessible) POST
https://www.cninfo.com.cn/new/hisAnnouncement/query
- Use a 30–90 day range
- Filter titles by keywords: 股东会/临时股东会/募投/关联交易/债券/科创债/发行/投资/战略投资/业绩/预增/预减
News (CN_A, best-effort):
- Primary: Eastmoney search (JSONP)
https://search-api-web.eastmoney.com/search/jsonp?cb=jQuery¶m={"keyword":"{code}","type":["cmsArticleWebOld"],"client":"web","clientType":"web","clientVersion":"curr","param":{"cmsArticleWebOld":{"searchScope":"default","sort":"default","pageIndex":1,"pageSize":10}}} - Fallback: GDELT Doc 2.0 (no key, mostly English results for CN stocks)
https://api.gdeltproject.org/api/v2/doc/doc?query=("{code}" OR "{name}")&mode=artlist&format=json&maxrecords=5×pan=1week&sort=datedesc
6) CN_A Narrative generation (deterministic)
Headline:
{name}({symbol}):{flow_summary},{event_summary}
flow_summary (from URL #2 main_net):
- main_net <= -5,000,000 => "主力资金净流出"
- main_net >= 5,000,000 => "主力资金净流入"
- |main_net| < 5,000,000 => "资金面波动不大"
- URL #2 unavailable => "资金面暂缺"
event_summary (from announcements/news):
- If titles contain 股东会/临时股东会 => "将召开(临时)股东会"
- Else if 债券/科创债/发行 => "发行债券"
- Else if 投资/战略投资 => "战略投资/对外投资"
- Else if 业绩/预增/预减 => "业绩预告"
- Else => "暂无重大公告"
^解读 narrative paragraph (ONE paragraph; no bullets): It MUST weave these facts in natural Chinese:
- 最新价格/收盘价与涨跌幅(from URL #1 close + change%)
- {target_date} 主力资金净流入/出 + 万元 + 占成交额%(from URL #2; ratio_pct)
- “从资金结构推断,散户可能呈现相反方向”(推断语气,不写成事实)
- 成交额(amount / 1e8 亿元)与换手率(turnover%)
- 量比句:仅当 (target_date == today) OR (evidence pack中已保存target_date量比) 才写 “量比为X”
Volume wording:
- If volume changes vs previous day available from URL #1 (compare last two klines): use 活跃/萎缩/持平
- Otherwise omit volume trend phrasing.
Event/news paragraphs:
- 取公告/新闻中最相关的 1–5 条,每条独立成段:
- “公司拟于{date}召开...”
- “{date},公司...”
- 若公告与新闻均为空:输出“近期暂无重要公告或新闻。”
- 若源不可达:输出“公告/新闻数据源暂不可达。”
7) US Brief recipe (succinct + robust)
Quote:
- Primary: Finnhub quote (requires FINNHUB_TOKEN)
https://finnhub.io/api/v1/quote?symbol={SYMBOL}&token={FINNHUB_TOKEN} - Fallback: Stooq CSV (EOD)
https://stooq.com/q/l/?s={symbol}.us&f=sd2t2ohlcv&h&e=csv
Events (best-effort):
- SEC EDGAR search-index OR submissions json if CIK available If unavailable: omit filings paragraph and rely on news.
News:
- Primary: Finnhub company-news (token)
- Fallback: GDELT
("{symbol}" OR "{company_name}")
Narrative:
- Same structure: headline + ^解读 + one paragraph + 0–5 event/news paragraphs.
- Keep it factual; do not fabricate filings.
8) CRYPTO Brief recipe
Quote: CoinGecko
https://api.coingecko.com/api/v3/simple/price?ids={id}&vs_currencies=usd&include_24hr_change=true&include_24hr_vol=true&include_market_cap=true
Optional daily reference:
https://api.coingecko.com/api/v3/coins/{id}/market_chart?vs_currency=usd&days=2&interval=daily
News:
- Primary: GDELT query for symbol/name
- Fallback: web_search if available
On-chain (optional):
- Etherscan only for ETH/ERC20 narratives; skip for BTC unless another provider is configured.
9) EvidencePack (MANDATORY, internal only)
Path:
{baseDir}/data/evidence_packs/B-{asset_id}-{YYYY-MM-DD}/v1.json
Rules:
- EvidencePack must be written even if some sources fail.
- For failed sources:
and includestatus="unavailable"
+attempted_url
.error
Minimal schema: { "pack_id": "B-{asset_id}-{YYYY-MM-DD}", "type": "narrative_brief", "target_date": "{YYYY-MM-DD}", "asof": "{iso}", "asset": { "market": "{market}", "symbol": "{symbol}", "name": "{name}" }, "evidences": [ { "evidence_id": "KLINE", "status": "ok|unavailable", "url_or_id": "...", "retrieved_at": "{iso}", "excerpt": "..." }, { "evidence_id": "FLOW", "status": "ok|unavailable", "url_or_id": "...", "retrieved_at": "{iso}", "excerpt": "..." }, { "evidence_id": "SNAP", "status": "ok|unavailable", "url_or_id": "...", "retrieved_at": "{iso}", "excerpt": "..." }, { "evidence_id": "ANN", "status": "ok|unavailable", "url_or_id": "...", "attempted_url": "...", "error": "...", "retrieved_at": "{iso}" }, { "evidence_id": "NEWS", "status": "ok|unavailable", "url_or_id": "...", "attempted_url": "...", "error": "...", "retrieved_at": "{iso}" } ], "claims": [] }
10) Commands
/ms setup feishu
- Detect Mode A/B and verify by sending a short test message/card.
/ms add <symbol> [name] [qty]
- Auto-detect market (CN_A / US / CRYPTO). Add to portfolios.json + create watch_rule with market defaults.
/ms remove <symbol>
- Remove from portfolios.json + delete corresponding watch_rule.
/ms portfolio import
- Parse multi-line text:
per line. Create portfolio + watch_rules for all.<symbol> [name] [qty]
/ms brief <symbol> [date=YYYY-MM-DD]
- Generate the narrative brief for that symbol (and optional date).
- CN_A data: visit all 4 URLs in section 5), or run
then read the JSON.python3 {baseDir}/fetch_cn.py {code} - Push to Feishu + save brief.md + save EvidencePack in same run.
/ms brief portfolio (or “昨日简报/组合简报”)
- Read watch_rules.json to get the asset list.
- Process assets one-by-one (never batch fetch across symbols): For each asset: build its evidence -> write its brief -> push+save -> next.
/ms watch start
- Create a silent monitor cron (no-deliver). No triggers => log only. Trigger => alert + push.
/ms digest start
- Create scheduled digest crons per market (see templates below). All no-deliver.
11) Cron templates (ALL no-deliver)
CN_A digest (15:00 CST):
openclaw cron add
--name "market-sentry:digest-cn"
--cron "0 15 * * 1-5"
--tz "Asia/Shanghai"
--session isolated
--no-deliver
--message "Run market-sentry CN_A digest. For each CN_A in watch_rules: generate narrative brief (strict format), push to Feishu, save brief+evidence pack. No chat output."
US digest (16:05 ET):
openclaw cron add
--name "market-sentry:digest-us"
--cron "5 16 * * 1-5"
--tz "America/New_York"
--session isolated
--no-deliver
--message "Run market-sentry US digest. For each US in watch_rules: generate narrative brief (strict format), push to Feishu, save brief+evidence pack. No chat output."
Monitor (every 5 min, silent):
openclaw cron add
--name "market-sentry:monitor"
--cron "*/5 * * * *"
--session isolated
--no-deliver
--message "Run market-sentry silent monitor. No triggers => log only and stop. Trigger => alert + evidence + push."
12) Degradation (must not block)
- If KLINE fails: output "行情数据暂不可获取,建议稍后再试。" and still push+save (EvidencePack records failure).
- If FLOW fails: replace flow_summary with "资金面暂缺" and omit ratio sentence; still push+save.
- If announcements/news fail: output exactly one paragraph "公告/新闻数据源暂不可达。" (or "近期暂无重要公告或新闻。") and still push+save.
- Never fabricate numbers, events, or news.