fix(FEAT-MO-001): 修复交易日对齐与延迟状态
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user