feat(xueqiu): add Danjuan fund account commands#391
feat(xueqiu): add Danjuan fund account commands#391jackwener merged 5 commits intojackwener:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Danjuan (danjuanfunds.com) fund account workflows under the existing opencli xueqiu command group, enabling browser-authenticated access to fund sub-accounts, holdings (including share fields), and snapshots, plus updated adapter documentation.
Changes:
- Add
fund-accounts,fund-holdings, andfund-snapshotbrowser commands that call Danjuan asset APIs using the existing browser session. - Include share-related holding fields (
volume,usableRemainShare) and account filtering via--accountin holdings. - Update Xueqiu browser adapter docs with Danjuan prerequisites and examples.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
src/clis/xueqiu/fund-accounts.yaml |
New command to list Danjuan fund sub-accounts from the assets gain endpoint. |
src/clis/xueqiu/fund-holdings.yaml |
New command to fetch per-account holdings (incl. shares) with optional account filtering. |
src/clis/xueqiu/fund-snapshot.yaml |
New command intended to produce a one-shot snapshot of totals + accounts + holdings. |
docs/adapters/browser/xueqiu.md |
Documents Danjuan login requirements and adds new command entries/examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (const acc of accounts) { | ||
| const accountId = String(acc?.invest_account_id || ''); | ||
| const detail = await fetchJson(`https://danjuanfunds.com/djapi/fundx/profit/assets/summary?invest_account_id=${encodeURIComponent(accountId)}`); | ||
| const data = detail?.data || {}; | ||
| const items = Array.isArray(data.items) ? data.items : []; |
There was a problem hiding this comment.
The per-account assets/summary fetch runs sequentially inside the for ... of loop. If a user has many sub-accounts, this can make fund-snapshot noticeably slow. Consider fetching summaries concurrently (e.g., Promise.all) with an optional concurrency limit to avoid overloading the endpoint.
| for (const acc of selected) { | ||
| const accountId = String(acc?.invest_account_id || ''); | ||
| const detail = await fetchJson(`https://danjuanfunds.com/djapi/fundx/profit/assets/summary?invest_account_id=${encodeURIComponent(accountId)}`); | ||
| const data = detail?.data || {}; | ||
| const funds = Array.isArray(data.items) ? data.items : []; |
There was a problem hiding this comment.
The per-account assets/summary fetch runs sequentially inside the for ... of loop. If --account matches multiple sub-accounts (or the user has many), this can be slow. Consider fetching account summaries concurrently (e.g., Promise.all) with a small concurrency limit.
| if (!selected.length) { | ||
| throw new Error(accountFilter ? `No account matched filter: ${accountFilter}` : '未找到基金子账户,可能未登录蛋卷资产页'); | ||
| } |
There was a problem hiding this comment.
This error message is English while nearby errors are Chinese, which makes UX inconsistent. Consider standardizing the language for this command (and include a login hint, similar to the earlier 未找到基金子账户... error).
| }); | ||
| } | ||
| } | ||
| if (!rows.length) throw new Error('No holdings found.'); |
There was a problem hiding this comment.
The "no holdings" error is generic (No holdings found.) and doesn’t help users distinguish between “account exists but has no positions” vs “API returned unexpected data / session issue”. Consider including the account id/name (and/or a login hint) in this error to make troubleshooting easier.
| if (!rows.length) throw new Error('No holdings found.'); | |
| if (!rows.length) { | |
| const accountSummary = selected | |
| .map((acc) => { | |
| const id = String(acc?.invest_account_id || ''); | |
| const name = String(acc?.invest_account_name || ''); | |
| if (name && id) return `${name} (${id})`; | |
| if (name) return name; | |
| return id || 'unknown account'; | |
| }) | |
| .join(', ') || 'unknown account(s)'; | |
| const filterInfo = accountFilter ? ` (filter: "${accountFilter}")` : ''; | |
| throw new Error( | |
| `No fund holdings returned for ${accountSummary}${filterInfo}. ` + | |
| 'This may mean the account currently has no fund positions, or there was an issue retrieving data (e.g., expired session or unexpected API response).' | |
| ); | |
| } |
| - map: | ||
| asOf: ${{ item.asOf }} | ||
| totalAssetAmount: ${{ item.totalAssetAmount }} | ||
| totalFundMarketValue: ${{ item.totalFundMarketValue }} | ||
| accountCount: ${{ item.accountCount }} | ||
| holdingCount: ${{ item.holdingCount }} | ||
|
|
There was a problem hiding this comment.
The map step drops accounts and holdings from the returned object. That means opencli xueqiu fund-snapshot -f json will not include the full snapshot details, which conflicts with the PR description/docs claiming it’s the easiest way to persist a full account snapshot. Consider passing through accounts/holdings in the map (or removing the map and relying on columns just for table output).
| accountCode: acc.invest_account_code, | ||
| marketValue: Number(acc.market_value), | ||
| dailyGain: Number(acc.daily_gain), | ||
| remindText: acc.remind_text || '', |
There was a problem hiding this comment.
marketValue / dailyGain are coerced with Number(...), which turns null into 0 and non-numeric strings into NaN. That can silently produce incorrect totals and odd table output. Prefer the num() helper pattern used in the other Danjuan commands (finite number => value, otherwise null).
| dailyGain: Number(acc.daily_gain), | ||
| remindText: acc.remind_text || '', | ||
| mainFlag: !!acc.main_flag, | ||
| totalFundMarketValue: root?.items?.find?.((item) => item?.summary_type === 'FUND')?.amount ?? null, |
There was a problem hiding this comment.
totalFundMarketValue recomputes root.items.find(...) even though fundSection was already found above. Using fundSection?.amount avoids duplication and reduces the chance of the two lookups diverging if the logic is changed later.
| totalFundMarketValue: root?.items?.find?.((item) => item?.summary_type === 'FUND')?.amount ?? null, | |
| totalFundMarketValue: fundSection?.amount ?? null, |
| const gain = await fetchJson('https://danjuanfunds.com/djapi/fundx/profit/assets/gain?gains=%5B%22private%22%5D'); | ||
| const root = gain?.data || {}; | ||
| const fundSection = (Array.isArray(root.items) ? root.items : []).find((item) => item?.summary_type === 'FUND'); | ||
| const accounts = Array.isArray(fundSection?.invest_account_list) ? fundSection.invest_account_list : []; |
There was a problem hiding this comment.
If fundSection is missing or invest_account_list is empty, this command currently returns a snapshot with accountCount: 0 / holdingCount: 0, which can mask a login/session problem (unlike fund-accounts / fund-holdings, which error when no accounts are found). Consider throwing a clear error when no fund accounts are detected so users don’t persist an “empty snapshot” by mistake.
| const accounts = Array.isArray(fundSection?.invest_account_list) ? fundSection.invest_account_list : []; | |
| const accounts = Array.isArray(fundSection?.invest_account_list) ? fundSection.invest_account_list : []; | |
| if (!accounts.length) { | |
| throw new Error('No fund accounts detected in snapshot. Hint: Are you logged in to danjuanfunds.com and do you have any fund accounts?'); | |
| } |
- Replace 3 YAML files with 4 TS files (shared utils + 3 commands) - Extract shared helpers: fetchDanjuanApi, fetchAssetGain, collectHoldings - Fix double-navigation by using navigateBefore instead of pipeline navigate - Unify error messages to English with Hint pattern - Mask real account ID in docs example - Add explicit default for --account arg
- Single page.evaluate with Promise.all for parallel account fetching (1 browser round-trip instead of N+1) - Merge fund-accounts into fund-holdings (account info visible per row) - 3 files: danjuan-utils.ts (shared), fund-holdings.ts, fund-snapshot.ts - Strong TypeScript interfaces for all data shapes - Update docs to reflect 2-command design
Summary
opencli xueqiu fund-accountsfor Danjuan fund sub-account overviewsopencli xueqiu fund-holdingsfor per-fund holdings + share fields (volume,usableRemainShare)opencli xueqiu fund-snapshotfor one-shot total asset + account + holdings snapshotsWhy
Xueqiu users often manage fund positions through Danjuan (
danjuanfunds.com). The stock-oriented commands already existed underxueqiu, but there was no browser-authenticated path for accessing the logged-in Danjuan fund account view.This change keeps the UX under the existing
xueqiucommand group while adding the fund-account workflows that users actually need for:Validation
npm run buildnode dist/main.js xueqiu --helpfundx/profit/assets/gainfundx/profit/assets/summaryNotes
xueqiu.comanddanjuanfunds.comfund-holdingssupports--accountfiltering by sub-account name or id