From 41d291077ecb8f838e610b55d4074416a2a05f43 Mon Sep 17 00:00:00 2001 From: Benjamin Liu Date: Tue, 12 May 2026 13:24:28 +0900 Subject: [PATCH 1/2] fix(xueqiu/kline,earnings-date): format dates in Asia/Shanghai instead of UTC (#1465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `xueqiu/kline` and `xueqiu/earnings-date` formatted bar timestamps with `new Date(ts).toISOString().split('T')[0]`. That string is the UTC calendar date, always one day earlier than the date xueqiu shows in its UI (which is Beijing-aligned for every market). Issue #1465 reports "5月10日跑的,5月8号的k线没有" because the May 8 China trading-day bar was labeled 2026-05-07. Same off-by-one was present in `earnings-date.js`. Routes both call sites through a new `formatChinaDate(ts)` helper in `clis/xueqiu/utils.js` built on `toLocaleDateString('en-CA', { timeZone: 'Asia/Shanghai' })`. Verified live against SZ300136 and AAPL: both now match the dates shown on xueqiu.com. Tests: `clis/xueqiu/utils.test.js` (new) pins the Asia/Shanghai semantic with 4 cases (China midnight, late-evening, 16:00 UTC day boundary, and nullish input). `npx vitest run --project adapter clis/xueqiu/` 49/49, `npx tsc --noEmit` clean, `npm run build` 815 entries unchanged shape. Closes #1465 --- clis/xueqiu/earnings-date.js | 4 ++-- clis/xueqiu/kline.js | 4 ++-- clis/xueqiu/utils.js | 7 +++++++ clis/xueqiu/utils.test.js | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 clis/xueqiu/utils.test.js diff --git a/clis/xueqiu/earnings-date.js b/clis/xueqiu/earnings-date.js index c83043aaa..743560a17 100644 --- a/clis/xueqiu/earnings-date.js +++ b/clis/xueqiu/earnings-date.js @@ -1,6 +1,6 @@ import { cli } from '@jackwener/opencli/registry'; import { EmptyResultError } from '@jackwener/opencli/errors'; -import { fetchXueqiuJson } from './utils.js'; +import { fetchXueqiuJson, formatChinaDate } from './utils.js'; cli({ site: 'xueqiu', name: 'earnings-date', @@ -32,7 +32,7 @@ cli({ .filter((item) => item.subtype === 2) .map((item) => { const ts = item.timestamp; - const dateStr = ts ? new Date(ts).toISOString().split('T')[0] : null; + const dateStr = ts ? formatChinaDate(ts) : null; const isFuture = ts && ts > now; return { date: dateStr, report: item.message, status: isFuture ? '⏳ 未发布' : '✅ 已发布', _ts: ts, _future: isFuture }; }); diff --git a/clis/xueqiu/kline.js b/clis/xueqiu/kline.js index 0fe8cabc2..7f44d4d38 100644 --- a/clis/xueqiu/kline.js +++ b/clis/xueqiu/kline.js @@ -1,6 +1,6 @@ import { cli } from '@jackwener/opencli/registry'; import { EmptyResultError } from '@jackwener/opencli/errors'; -import { fetchXueqiuJson } from './utils.js'; +import { fetchXueqiuJson, formatChinaDate } from './utils.js'; cli({ site: 'xueqiu', name: 'kline', @@ -31,7 +31,7 @@ cli({ const colIdx = {}; columns.forEach((name, i) => { colIdx[name] = i; }); return d.data.item.map(row => ({ - date: colIdx.timestamp != null ? new Date(row[colIdx.timestamp]).toISOString().split('T')[0] : null, + date: colIdx.timestamp != null ? formatChinaDate(row[colIdx.timestamp]) : null, open: row[colIdx.open] ?? null, high: row[colIdx.high] ?? null, low: row[colIdx.low] ?? null, diff --git a/clis/xueqiu/utils.js b/clis/xueqiu/utils.js index 5986b0555..2304cbf69 100644 --- a/clis/xueqiu/utils.js +++ b/clis/xueqiu/utils.js @@ -1,4 +1,11 @@ import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors'; + +/** Format a Unix ms timestamp as the matching `YYYY-MM-DD` in Asia/Shanghai (xueqiu's canonical user timezone for all markets). */ +export function formatChinaDate(ts) { + if (ts == null) return null; + return new Date(ts).toLocaleDateString('en-CA', { timeZone: 'Asia/Shanghai' }); +} + /** * Fetch a xueqiu JSON API from inside the browser context (credentials included). * Page must already be navigated to xueqiu.com before calling this function. diff --git a/clis/xueqiu/utils.test.js b/clis/xueqiu/utils.test.js new file mode 100644 index 000000000..78bb91d79 --- /dev/null +++ b/clis/xueqiu/utils.test.js @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { formatChinaDate } from './utils.js'; + +describe('formatChinaDate', () => { + it('returns the Asia/Shanghai date for a UTC ms at China midnight', () => { + expect(formatChinaDate(Date.UTC(2026, 4, 7, 16, 0, 0))).toBe('2026-05-08'); + }); + it('returns the same China date for a moment late in the day', () => { + expect(formatChinaDate(Date.UTC(2026, 4, 8, 14, 0, 0))).toBe('2026-05-08'); + }); + it('crosses the China day boundary at 16:00 UTC', () => { + expect(formatChinaDate(Date.UTC(2026, 0, 1, 15, 59, 59))).toBe('2026-01-01'); + expect(formatChinaDate(Date.UTC(2026, 0, 1, 16, 0, 0))).toBe('2026-01-02'); + }); + it('returns null for nullish input', () => { + expect(formatChinaDate(null)).toBeNull(); + expect(formatChinaDate(undefined)).toBeNull(); + }); +}); From c0c3f849254ebc702ef9a97272f71b2dd39ce9a9 Mon Sep 17 00:00:00 2001 From: jackwener Date: Wed, 13 May 2026 17:34:03 +0800 Subject: [PATCH 2/2] fix(xueqiu): stabilize China date formatting --- clis/xueqiu/utils.js | 14 +++++++++++++- clis/xueqiu/utils.test.js | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/clis/xueqiu/utils.js b/clis/xueqiu/utils.js index 2304cbf69..f80f0ed7d 100644 --- a/clis/xueqiu/utils.js +++ b/clis/xueqiu/utils.js @@ -1,9 +1,21 @@ import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors'; +const CHINA_DATE_FORMATTER = new Intl.DateTimeFormat('en-US', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', +}); + /** Format a Unix ms timestamp as the matching `YYYY-MM-DD` in Asia/Shanghai (xueqiu's canonical user timezone for all markets). */ export function formatChinaDate(ts) { if (ts == null) return null; - return new Date(ts).toLocaleDateString('en-CA', { timeZone: 'Asia/Shanghai' }); + const parts = Object.fromEntries( + CHINA_DATE_FORMATTER.formatToParts(new Date(ts)) + .filter((part) => part.type !== 'literal') + .map((part) => [part.type, part.value]), + ); + return `${parts.year}-${parts.month}-${parts.day}`; } /** diff --git a/clis/xueqiu/utils.test.js b/clis/xueqiu/utils.test.js index 78bb91d79..3bc6c2ae2 100644 --- a/clis/xueqiu/utils.test.js +++ b/clis/xueqiu/utils.test.js @@ -8,10 +8,17 @@ describe('formatChinaDate', () => { it('returns the same China date for a moment late in the day', () => { expect(formatChinaDate(Date.UTC(2026, 4, 8, 14, 0, 0))).toBe('2026-05-08'); }); + it('formats representative A-share and US-market bars on xueqiu Beijing dates', () => { + expect(formatChinaDate(Date.UTC(2026, 4, 7, 16, 0, 0))).toBe('2026-05-08'); + expect(formatChinaDate(Date.UTC(2026, 4, 10, 16, 0, 0))).toBe('2026-05-11'); + }); it('crosses the China day boundary at 16:00 UTC', () => { expect(formatChinaDate(Date.UTC(2026, 0, 1, 15, 59, 59))).toBe('2026-01-01'); expect(formatChinaDate(Date.UTC(2026, 0, 1, 16, 0, 0))).toBe('2026-01-02'); }); + it('always returns an ISO calendar date string, not a locale-shaped slash date', () => { + expect(formatChinaDate(Date.UTC(2026, 0, 1, 16, 0, 0))).toMatch(/^\d{4}-\d{2}-\d{2}$/); + }); it('returns null for nullish input', () => { expect(formatChinaDate(null)).toBeNull(); expect(formatChinaDate(undefined)).toBeNull();