Skip to content

feat(xueqiu): add Danjuan fund account commands#391

Merged
jackwener merged 5 commits intojackwener:mainfrom
iridite:feat/xueqiu-danjuan-funds
Mar 25, 2026
Merged

feat(xueqiu): add Danjuan fund account commands#391
jackwener merged 5 commits intojackwener:mainfrom
iridite:feat/xueqiu-danjuan-funds

Conversation

@iridite
Copy link
Copy Markdown
Contributor

@iridite iridite commented Mar 24, 2026

Summary

  • add opencli xueqiu fund-accounts for Danjuan fund sub-account overviews
  • add opencli xueqiu fund-holdings for per-fund holdings + share fields (volume, usableRemainShare)
  • add opencli xueqiu fund-snapshot for one-shot total asset + account + holdings snapshots
  • document Danjuan login prerequisites and usage examples in the Xueqiu browser adapter docs

Why

Xueqiu users often manage fund positions through Danjuan (danjuanfunds.com). The stock-oriented commands already existed under xueqiu, but there was no browser-authenticated path for accessing the logged-in Danjuan fund account view.

This change keeps the UX under the existing xueqiu command group while adding the fund-account workflows that users actually need for:

  • account overview / allocation review
  • holding-level share inspection
  • periodic snapshots for later analysis or diffing

Validation

  • npm run build
  • verified new commands appear in node dist/main.js xueqiu --help
  • command logic was exercised against a logged-in Danjuan browser session during development, including:
    • total asset overview via fundx/profit/assets/gain
    • account holdings via fundx/profit/assets/summary

Notes

  • fund commands require the Browser Bridge session to be logged into both xueqiu.com and danjuanfunds.com
  • fund-holdings supports --account filtering by sub-account name or id

Copilot AI review requested due to automatic review settings March 24, 2026 17:05
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and fund-snapshot browser commands that call Danjuan asset APIs using the existing browser session.
  • Include share-related holding fields (volume, usableRemainShare) and account filtering via --account in 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.

Comment thread src/clis/xueqiu/fund-snapshot.yaml Outdated
Comment on lines +27 to +31
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 : [];
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/clis/xueqiu/fund-holdings.yaml Outdated
Comment on lines +42 to +46
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 : [];
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/clis/xueqiu/fund-holdings.yaml Outdated
Comment on lines +37 to +39
if (!selected.length) {
throw new Error(accountFilter ? `No account matched filter: ${accountFilter}` : '未找到基金子账户,可能未登录蛋卷资产页');
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread src/clis/xueqiu/fund-holdings.yaml Outdated
});
}
}
if (!rows.length) throw new Error('No holdings found.');
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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).'
);
}

Copilot uses AI. Check for mistakes.
Comment thread src/clis/xueqiu/fund-snapshot.yaml Outdated
Comment on lines +71 to +77
- map:
asOf: ${{ item.asOf }}
totalAssetAmount: ${{ item.totalAssetAmount }}
totalFundMarketValue: ${{ item.totalFundMarketValue }}
accountCount: ${{ item.accountCount }}
holdingCount: ${{ item.holdingCount }}

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread src/clis/xueqiu/fund-accounts.yaml Outdated
Comment on lines +22 to +25
accountCode: acc.invest_account_code,
marketValue: Number(acc.market_value),
dailyGain: Number(acc.daily_gain),
remindText: acc.remind_text || '',
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread src/clis/xueqiu/fund-accounts.yaml Outdated
dailyGain: Number(acc.daily_gain),
remindText: acc.remind_text || '',
mainFlag: !!acc.main_flag,
totalFundMarketValue: root?.items?.find?.((item) => item?.summary_type === 'FUND')?.amount ?? null,
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
totalFundMarketValue: root?.items?.find?.((item) => item?.summary_type === 'FUND')?.amount ?? null,
totalFundMarketValue: fundSection?.amount ?? null,

Copilot uses AI. Check for mistakes.
Comment thread src/clis/xueqiu/fund-snapshot.yaml Outdated
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 : [];
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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?');
}

Copilot uses AI. Check for mistakes.
- 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
@jackwener jackwener merged commit 9c99cbf into jackwener:main Mar 25, 2026
22 checks passed
@iridite iridite deleted the feat/xueqiu-danjuan-funds branch March 25, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants