Skip to content

Commit 6af4db2

Browse files
fix(xueqiu/kline,earnings-date): format dates in Asia/Shanghai instead of UTC (#1498)
* fix(xueqiu/kline,earnings-date): format dates in Asia/Shanghai instead of UTC (#1465) `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 * fix(xueqiu): stabilize China date formatting --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent 5127211 commit 6af4db2

4 files changed

Lines changed: 49 additions & 4 deletions

File tree

clis/xueqiu/earnings-date.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cli } from '@jackwener/opencli/registry';
22
import { EmptyResultError } from '@jackwener/opencli/errors';
3-
import { fetchXueqiuJson } from './utils.js';
3+
import { fetchXueqiuJson, formatChinaDate } from './utils.js';
44
cli({
55
site: 'xueqiu',
66
name: 'earnings-date',
@@ -32,7 +32,7 @@ cli({
3232
.filter((item) => item.subtype === 2)
3333
.map((item) => {
3434
const ts = item.timestamp;
35-
const dateStr = ts ? new Date(ts).toISOString().split('T')[0] : null;
35+
const dateStr = ts ? formatChinaDate(ts) : null;
3636
const isFuture = ts && ts > now;
3737
return { date: dateStr, report: item.message, status: isFuture ? '⏳ 未发布' : '✅ 已发布', _ts: ts, _future: isFuture };
3838
});

clis/xueqiu/kline.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cli } from '@jackwener/opencli/registry';
22
import { EmptyResultError } from '@jackwener/opencli/errors';
3-
import { fetchXueqiuJson } from './utils.js';
3+
import { fetchXueqiuJson, formatChinaDate } from './utils.js';
44
cli({
55
site: 'xueqiu',
66
name: 'kline',
@@ -31,7 +31,7 @@ cli({
3131
const colIdx = {};
3232
columns.forEach((name, i) => { colIdx[name] = i; });
3333
return d.data.item.map(row => ({
34-
date: colIdx.timestamp != null ? new Date(row[colIdx.timestamp]).toISOString().split('T')[0] : null,
34+
date: colIdx.timestamp != null ? formatChinaDate(row[colIdx.timestamp]) : null,
3535
open: row[colIdx.open] ?? null,
3636
high: row[colIdx.high] ?? null,
3737
low: row[colIdx.low] ?? null,

clis/xueqiu/utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
11
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
2+
3+
const CHINA_DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
4+
timeZone: 'Asia/Shanghai',
5+
year: 'numeric',
6+
month: '2-digit',
7+
day: '2-digit',
8+
});
9+
10+
/** Format a Unix ms timestamp as the matching `YYYY-MM-DD` in Asia/Shanghai (xueqiu's canonical user timezone for all markets). */
11+
export function formatChinaDate(ts) {
12+
if (ts == null) return null;
13+
const parts = Object.fromEntries(
14+
CHINA_DATE_FORMATTER.formatToParts(new Date(ts))
15+
.filter((part) => part.type !== 'literal')
16+
.map((part) => [part.type, part.value]),
17+
);
18+
return `${parts.year}-${parts.month}-${parts.day}`;
19+
}
20+
221
/**
322
* Fetch a xueqiu JSON API from inside the browser context (credentials included).
423
* Page must already be navigated to xueqiu.com before calling this function.

clis/xueqiu/utils.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { formatChinaDate } from './utils.js';
3+
4+
describe('formatChinaDate', () => {
5+
it('returns the Asia/Shanghai date for a UTC ms at China midnight', () => {
6+
expect(formatChinaDate(Date.UTC(2026, 4, 7, 16, 0, 0))).toBe('2026-05-08');
7+
});
8+
it('returns the same China date for a moment late in the day', () => {
9+
expect(formatChinaDate(Date.UTC(2026, 4, 8, 14, 0, 0))).toBe('2026-05-08');
10+
});
11+
it('formats representative A-share and US-market bars on xueqiu Beijing dates', () => {
12+
expect(formatChinaDate(Date.UTC(2026, 4, 7, 16, 0, 0))).toBe('2026-05-08');
13+
expect(formatChinaDate(Date.UTC(2026, 4, 10, 16, 0, 0))).toBe('2026-05-11');
14+
});
15+
it('crosses the China day boundary at 16:00 UTC', () => {
16+
expect(formatChinaDate(Date.UTC(2026, 0, 1, 15, 59, 59))).toBe('2026-01-01');
17+
expect(formatChinaDate(Date.UTC(2026, 0, 1, 16, 0, 0))).toBe('2026-01-02');
18+
});
19+
it('always returns an ISO calendar date string, not a locale-shaped slash date', () => {
20+
expect(formatChinaDate(Date.UTC(2026, 0, 1, 16, 0, 0))).toMatch(/^\d{4}-\d{2}-\d{2}$/);
21+
});
22+
it('returns null for nullish input', () => {
23+
expect(formatChinaDate(null)).toBeNull();
24+
expect(formatChinaDate(undefined)).toBeNull();
25+
});
26+
});

0 commit comments

Comments
 (0)