fix(FEAT-MO-001): 修复交易日对齐与延迟状态

This commit is contained in:
2026-08-18 20:12:26 +08:00
parent 49f6be9c43
commit 7e4dcd9232
15 changed files with 396 additions and 58 deletions
@@ -42,6 +42,25 @@ function createSnapshotMeta(): Pick<
return { snapshotId, asOf, source: 'tdx' };
}
/** 交易日状态统一按上海日历判断;休市日保守标记 delayed,避免把历史值冒充实时。 */
function getShanghaiCalendarDate(asOf: string): string {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(new Date(asOf));
const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
return `${values.year}${values.month}${values.day}`;
}
function createTradeDateWarning(subject: string, tradeDate: string | undefined, asOf: string) {
if (!tradeDate) return `${subject}交易日期无法确认`;
return tradeDate === getShanghaiCalendarDate(asOf)
? null
: `${subject}交易日期为 ${tradeDate},非当前日期`;
}
@Injectable()
export class MarketOverviewService {
constructor(@Inject(TDX_CLIENT_PORT) private readonly tdxClient: TdxClient) {}
@@ -56,12 +75,19 @@ export class MarketOverviewService {
period: '1d',
});
const items = mapIndexQuotes(result, CORE_INDEXES);
const status = items.length === CORE_INDEXES.length ? 'fresh' : 'partial';
const tradeDateWarning = createTradeDateWarning('核心指数', items[0]?.tradeDate, meta.asOf);
const status =
items.length !== CORE_INDEXES.length ? 'partial' : tradeDateWarning ? 'delayed' : 'fresh';
return {
...meta,
status,
data: { items },
warnings: status === 'partial' ? ['部分核心指数未返回有效数据'] : [],
warnings:
status === 'partial'
? ['部分核心指数未返回有效数据']
: tradeDateWarning
? [tradeDateWarning]
: [],
};
} catch (error) {
return {
@@ -105,13 +131,23 @@ export class MarketOverviewService {
async getBreadth(): Promise<MarketOverviewResponse<BreadthDataDto>> {
const meta = createSnapshotMeta();
const results = await Promise.allSettled(
BREADTH_INDEXES.map((item) =>
this.tdxClient.request<TdxMarketSnapshotResult>('get_market_snapshot', {
stock_code: item.code,
}),
const [results, [referenceResult]] = await Promise.all([
Promise.allSettled(
BREADTH_INDEXES.map((item) =>
this.tdxClient.request<TdxMarketSnapshotResult>('get_market_snapshot', {
stock_code: item.code,
}),
),
),
);
Promise.allSettled([
this.tdxClient.request<TdxMarketDataResult>('get_market_data', {
stock_list: [CORE_INDEXES[0].code],
count: 1,
dividend_type: 'none',
period: '1d',
}),
]),
]);
const validSnapshots = results.flatMap((result) => {
if (result.status === 'rejected') return [];
return mapBreadthSnapshots([result.value]).validCount === 1 ? [result.value] : [];
@@ -133,13 +169,27 @@ export class MarketOverviewService {
warnings: [`市场宽度数据不可用:${unavailableMarkets.join('、')}`],
};
}
const status = mapped.validCount === BREADTH_INDEXES.length ? 'fresh' : 'partial';
const referenceTradeDate =
referenceResult?.status === 'fulfilled'
? mapIndexQuotes(referenceResult.value, [CORE_INDEXES[0]])[0]?.tradeDate
: undefined;
const tradeDateWarning = createTradeDateWarning('市场宽度', referenceTradeDate, meta.asOf);
const status =
mapped.validCount !== BREADTH_INDEXES.length
? 'partial'
: tradeDateWarning
? 'delayed'
: 'fresh';
return {
...meta,
status,
data: mapped.data,
warnings:
status === 'partial' ? [`部分市场涨跌家数不可用:${unavailableMarkets.join('、')}`] : [],
status === 'partial'
? [`部分市场涨跌家数不可用:${unavailableMarkets.join('、')}`]
: tradeDateWarning
? [tradeDateWarning]
: [],
};
}
@@ -167,12 +217,19 @@ export class MarketOverviewService {
});
const mappedItems = mapSectorQuotes(result, definitions);
const items = mappedItems.slice(0, 8);
const tradeDateWarning = createTradeDateWarning(
'一级行业',
mappedItems[0]?.tradeDate,
meta.asOf,
);
const status =
mappedItems.length === definitions.length
? 'fresh'
: mappedItems.length
mappedItems.length === 0
? 'unavailable'
: mappedItems.length !== definitions.length
? 'partial'
: 'unavailable';
: tradeDateWarning
? 'delayed'
: 'fresh';
return {
...meta,
status,
@@ -180,7 +237,11 @@ export class MarketOverviewService {
warnings:
status === 'fresh'
? []
: [status === 'partial' ? '部分一级行业未返回有效行情' : '一级行业行情不可用'],
: status === 'partial'
? ['部分一级行业未返回有效行情']
: status === 'delayed' && tradeDateWarning
? [tradeDateWarning]
: ['一级行业行情不可用'],
};
} catch (error) {
return {
@@ -204,7 +265,13 @@ export class MarketOverviewService {
const mapped = mapTurnoverTrend(result, TURNOVER_INDEXES);
if (!mapped.data)
return { ...meta, status: 'unavailable', data: null, warnings: ['成交额趋势数据不完整'] };
const status = mapped.validMarketCount === TURNOVER_INDEXES.length ? 'fresh' : 'partial';
const tradeDateWarning = createTradeDateWarning('成交额', mapped.data.tradeDate, meta.asOf);
const status =
mapped.validMarketCount !== TURNOVER_INDEXES.length
? 'partial'
: tradeDateWarning
? 'delayed'
: 'fresh';
return {
...meta,
status,
@@ -212,7 +279,9 @@ export class MarketOverviewService {
warnings:
status === 'partial'
? [`部分市场成交额不可用,当前汇总 ${mapped.validMarketCount} 个市场`]
: [],
: tradeDateWarning
? [tradeDateWarning]
: [],
};
} catch (error) {
return {