From 5324f31ff30a5f00fa5e23346bf7b0c69200c126 Mon Sep 17 00:00:00 2001 From: williamxie1989 <21219102@qq.com> Date: Sun, 5 Apr 2026 19:06:46 +0800 Subject: [PATCH 1/3] feat(xueqiu): add kline and groups adapters Add kline.yaml: fetch candlestick/OHLCV data from Xueqiu v5 chart API. Supports custom days lookback and outputs date, open, high, low, close, volume, percent. Add groups.yaml: list Xueqiu portfolio/group entries. Co-Authored-By: Claude --- clis/xueqiu/groups.yaml | 37 +++++++++++++++++++++++ clis/xueqiu/kline.yaml | 65 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 clis/xueqiu/groups.yaml create mode 100644 clis/xueqiu/kline.yaml diff --git a/clis/xueqiu/groups.yaml b/clis/xueqiu/groups.yaml new file mode 100644 index 000000000..996732269 --- /dev/null +++ b/clis/xueqiu/groups.yaml @@ -0,0 +1,37 @@ +site: xueqiu +name: groups +description: 获取雪球自选股分组列表 +domain: xueqiu.com +browser: true + +args: + category: + type: str + default: "1" + description: "分类:1=自选(默认) 2=持仓 3=关注" + +pipeline: + - navigate: https://xueqiu.com + - evaluate: | + (async () => { + const category = parseInt(${{ args.category | json }}) || 1; + const resp = await fetch(`https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json?size=1&category=${category}&pid=-1`, {credentials: 'include'}); + if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?'); + const d = await resp.json(); + if (!d.data) throw new Error('获取失败,可能未登录'); + + const groups = d.data.groups || []; + // 添加"全部"选项 + const result = [{ pid: '-1', name: '全部', count: d.data.total || 0 }]; + + for (const g of groups) { + result.push({ + pid: String(g.id), + name: g.name, + count: g.count || 0 + }); + } + return result; + })() + +columns: [pid, name, count] \ No newline at end of file diff --git a/clis/xueqiu/kline.yaml b/clis/xueqiu/kline.yaml new file mode 100644 index 000000000..fa63d1632 --- /dev/null +++ b/clis/xueqiu/kline.yaml @@ -0,0 +1,65 @@ +site: xueqiu +name: kline +description: 获取雪球股票K线(历史行情)数据 +domain: xueqiu.com +browser: true + +args: + symbol: + positional: true + type: str + required: true + description: 股票代码,如 SH600519、SZ000858、AAPL + days: + type: int + default: 14 + description: 回溯天数(默认14天) + +pipeline: + - navigate: https://xueqiu.com + + - evaluate: | + (async () => { + const symbol = (${{ args.symbol | json }} || '').toUpperCase(); + const days = parseInt(${{ args.days | json }}) || 14; + if (!symbol) throw new Error('Missing argument: symbol'); + + // begin = now minus days (for count=-N, returns N items ending at begin) + const beginTs = Date.now(); + const resp = await fetch('https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol=' + encodeURIComponent(symbol) + '&begin=' + beginTs + '&period=day&type=before&count=-' + days, {credentials: 'include'}); + if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?'); + const d = await resp.json(); + + if (!d.data || !d.data.item || d.data.item.length === 0) return []; + + const columns = d.data.column || []; + const items = d.data.item || []; + const colIdx = {}; + columns.forEach((name, i) => { colIdx[name] = i; }); + + function fmt(v) { return v == null ? null : v; } + + return items.map(row => ({ + date: colIdx.timestamp != null ? new Date(row[colIdx.timestamp]).toISOString().split('T')[0] : null, + open: fmt(row[colIdx.open]), + high: fmt(row[colIdx.high]), + low: fmt(row[colIdx.low]), + close: fmt(row[colIdx.close]), + volume: fmt(row[colIdx.volume]), + amount: fmt(row[colIdx.amount]), + chg: fmt(row[colIdx.chg]), + percent: fmt(row[colIdx.percent]), + symbol: symbol + })); + })() + + - map: + date: ${{ item.date }} + open: ${{ item.open }} + high: ${{ item.high }} + low: ${{ item.low }} + close: ${{ item.close }} + volume: ${{ item.volume }} + percent: ${{ item.percent }} + +columns: [date, open, high, low, close, volume] From 44efe566babb30b3914cea004bf21d36b0ce5c40 Mon Sep 17 00:00:00 2001 From: williamxie1989 <21219102@qq.com> Date: Sun, 5 Apr 2026 23:43:34 +0800 Subject: [PATCH 2/3] fix(xueqiu): correct groups.yaml to use /portfolio/list.json API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation used /portfolio/stock/list.json which only returns stocks in a single group and does not return the group list. Switch to /portfolio/list.json which returns all portfolio groups including 实盘, 沪深, 港股, 美股, 模拟(pid=-4), 持仓 etc. Co-Authored-By: Claude Sonnet 4.6 --- clis/xueqiu/groups.yaml | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/clis/xueqiu/groups.yaml b/clis/xueqiu/groups.yaml index 996732269..62d6214aa 100644 --- a/clis/xueqiu/groups.yaml +++ b/clis/xueqiu/groups.yaml @@ -1,37 +1,23 @@ site: xueqiu name: groups -description: 获取雪球自选股分组列表 +description: 获取雪球自选股分组列表(含模拟组合) domain: xueqiu.com browser: true -args: - category: - type: str - default: "1" - description: "分类:1=自选(默认) 2=持仓 3=关注" - pipeline: - navigate: https://xueqiu.com - evaluate: | (async () => { - const category = parseInt(${{ args.category | json }}) || 1; - const resp = await fetch(`https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json?size=1&category=${category}&pid=-1`, {credentials: 'include'}); + const resp = await fetch('https://stock.xueqiu.com/v5/stock/portfolio/list.json?category=1&size=20', {credentials: 'include'}); if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?'); const d = await resp.json(); - if (!d.data) throw new Error('获取失败,可能未登录'); - - const groups = d.data.groups || []; - // 添加"全部"选项 - const result = [{ pid: '-1', name: '全部', count: d.data.total || 0 }]; + if (!d.data || !d.data.stocks) throw new Error('获取失败,可能未登录'); - for (const g of groups) { - result.push({ - pid: String(g.id), - name: g.name, - count: g.count || 0 - }); - } - return result; + return d.data.stocks.map(g => ({ + pid: String(g.id), + name: g.name, + count: g.symbol_count || 0 + })); })() -columns: [pid, name, count] \ No newline at end of file +columns: [pid, name, count] From 999368b575f11d3d41407d7caa90811125eece1b Mon Sep 17 00:00:00 2001 From: williamxie1989 <21219102@qq.com> Date: Sun, 5 Apr 2026 23:54:23 +0800 Subject: [PATCH 3/3] fix(xueqiu): replace watchlist category param with pid selector - Remove the unused 'category' parameter (the API ignores it; all groups live under category=1 regardless) - Replace with 'pid' parameter to allow fetching any group: -4=simulated, -5=SH/SZ, -6=US stocks, -7=HK stocks, etc. - API path still uses category=1 but pid is now user-controllable Co-Authored-By: Claude Sonnet 4.6 --- clis/xueqiu/watchlist.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/clis/xueqiu/watchlist.yaml b/clis/xueqiu/watchlist.yaml index 7304f73d8..b20eb9e10 100644 --- a/clis/xueqiu/watchlist.yaml +++ b/clis/xueqiu/watchlist.yaml @@ -1,14 +1,14 @@ site: xueqiu name: watchlist -description: 获取雪球自选股列表 +description: 获取雪球自选股/模拟组合股票列表 domain: xueqiu.com browser: true args: - category: - type: str # using str to prevent parsing issues like 01 - default: "1" - description: "分类:1=自选(默认) 2=持仓 3=关注" + pid: + type: str + default: "-1" + description: "分组ID:-1=全部(默认) -4=模拟 -5=沪深 -6=美股 -7=港股 -10=实盘 0=持仓(通过 xueqiu groups 获取)" limit: type: int default: 100 @@ -18,12 +18,12 @@ pipeline: - navigate: https://xueqiu.com - evaluate: | (async () => { - const category = parseInt(${{ args.category | json }}) || 1; - const resp = await fetch(`https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json?size=100&category=${category}&pid=-1`, {credentials: 'include'}); + const pid = ${{ args.pid | json }} || '-1'; + const resp = await fetch(`https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json?size=100&category=1&pid=${encodeURIComponent(pid)}`, {credentials: 'include'}); if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?'); const d = await resp.json(); if (!d.data || !d.data.stocks) throw new Error('获取失败,可能未登录'); - + return d.data.stocks.map(s => ({ symbol: s.symbol, name: s.name, @@ -40,7 +40,7 @@ pipeline: name: ${{ item.name }} price: ${{ item.price }} changePercent: ${{ item.changePercent }} - + - limit: ${{ args.limit }} columns: [symbol, name, price, changePercent]