fix(FEAT-MO-001): 修复交易日对齐与延迟状态
This commit is contained in:
@@ -95,13 +95,30 @@ describe('市场概览服务', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('核心指数交易日旧于上海当前日期时标记 delayed', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-08-19T03:00:00.000Z'));
|
||||
try {
|
||||
const client = { request: vi.fn().mockResolvedValue(indexResult) };
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getIndices();
|
||||
|
||||
expect(response.status).toBe('delayed');
|
||||
expect(response.warnings).toEqual(['核心指数交易日期为 20260818,非当前日期']);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('汇总三个市场指数的上涨和下跌家数并保留未知平盘数', async () => {
|
||||
const client = {
|
||||
request: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(breadthSnapshot('10', '5'))
|
||||
.mockResolvedValueOnce(breadthSnapshot('20', '15'))
|
||||
.mockResolvedValueOnce(breadthSnapshot('3', '2')),
|
||||
.mockResolvedValueOnce(breadthSnapshot('3', '2'))
|
||||
.mockResolvedValueOnce(indexResult),
|
||||
};
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
@@ -124,7 +141,8 @@ describe('市场概览服务', () => {
|
||||
.fn()
|
||||
.mockResolvedValueOnce(breadthSnapshot('10', '5'))
|
||||
.mockResolvedValueOnce({ ErrorId: '0', UpHome: '', DownHome: '3' })
|
||||
.mockRejectedValueOnce(new Error('raw network detail')),
|
||||
.mockRejectedValueOnce(new Error('raw network detail'))
|
||||
.mockResolvedValueOnce(indexResult),
|
||||
};
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
@@ -138,6 +156,31 @@ describe('市场概览服务', () => {
|
||||
expect(response.warnings.join(' ')).not.toContain('raw network detail');
|
||||
});
|
||||
|
||||
it('市场宽度使用核心指数交易日识别 delayed,不把旧快照标为 fresh', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-08-18T03:00:00.000Z'));
|
||||
try {
|
||||
const staleIndexResult = structuredClone(indexResult);
|
||||
staleIndexResult.Value['000001.SH'].Date = ['20260816', '20260817'];
|
||||
const client = {
|
||||
request: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(breadthSnapshot('10', '5'))
|
||||
.mockResolvedValueOnce(breadthSnapshot('20', '15'))
|
||||
.mockResolvedValueOnce(breadthSnapshot('3', '2'))
|
||||
.mockResolvedValueOnce(staleIndexResult),
|
||||
};
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getBreadth();
|
||||
|
||||
expect(response.status).toBe('delayed');
|
||||
expect(response.warnings).toEqual(['市场宽度交易日期为 20260817,非当前日期']);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('按一级行业涨跌幅排序板块并保留 TDX 代码', async () => {
|
||||
const client = {
|
||||
request: vi.fn().mockResolvedValueOnce(sectorResult).mockResolvedValueOnce(sectorKlineResult),
|
||||
@@ -202,6 +245,44 @@ describe('市场概览服务', () => {
|
||||
expect(response.data?.items[0].name).toBe('行业8');
|
||||
});
|
||||
|
||||
it('一级行业混合交易日时返回 partial,不把不同日期误标 fresh', async () => {
|
||||
const mixedDates = structuredClone(sectorKlineResult);
|
||||
mixedDates.Value['881355.SH'].Date = ['20260818', '20260819'];
|
||||
const client = {
|
||||
request: vi.fn().mockResolvedValueOnce(sectorResult).mockResolvedValueOnce(mixedDates),
|
||||
};
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getSectors();
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'partial',
|
||||
data: { items: [expect.objectContaining({ code: '881355.SH', tradeDate: '20260819' })] },
|
||||
});
|
||||
expect(response.warnings).toEqual(['部分一级行业未返回有效行情']);
|
||||
});
|
||||
|
||||
it('一级行业交易日旧于上海当前日期时标记 delayed', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-08-19T03:00:00.000Z'));
|
||||
try {
|
||||
const client = {
|
||||
request: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(sectorResult)
|
||||
.mockResolvedValueOnce(sectorKlineResult),
|
||||
};
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getSectors();
|
||||
|
||||
expect(response.status).toBe('delayed');
|
||||
expect(response.warnings).toEqual(['一级行业交易日期为 20260818,非当前日期']);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('按交易所指数成交额计算亿元级趋势而不宣称主力净流入', async () => {
|
||||
const client = { request: vi.fn().mockResolvedValue(turnoverResult) };
|
||||
const service = new MarketOverviewService(client);
|
||||
@@ -238,4 +319,20 @@ describe('市场概览服务', () => {
|
||||
});
|
||||
expect(response.warnings).toEqual(['部分市场成交额不可用,当前汇总 2 个市场']);
|
||||
});
|
||||
|
||||
it('成交额交易日旧于上海当前日期时标记 delayed', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-08-19T03:00:00.000Z'));
|
||||
try {
|
||||
const client = { request: vi.fn().mockResolvedValue(turnoverResult) };
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getFunds();
|
||||
|
||||
expect(response.status).toBe('delayed');
|
||||
expect(response.warnings).toEqual(['成交额交易日期为 20260818,非当前日期']);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -87,6 +87,55 @@ describe('TDX 行情映射', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('市场宽度拒绝小数和十六进制字符串,避免把非计数值纳入汇总', () => {
|
||||
expect(
|
||||
mapBreadthSnapshots([
|
||||
{ UpHome: '1.5', DownHome: '5' },
|
||||
{ UpHome: '0x10', DownHome: '2' },
|
||||
{ UpHome: '1e3', DownHome: '2' },
|
||||
{ UpHome: '9007199254740992', DownHome: '2' },
|
||||
{ UpHome: '10', DownHome: '5' },
|
||||
]),
|
||||
).toEqual({
|
||||
data: { advancing: 10, declining: 5, unchanged: null },
|
||||
validCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('核心指数混合交易日时只保留最新交易日,避免跨日组合', () => {
|
||||
const result = mapIndexQuotes(
|
||||
{
|
||||
ErrorId: '0',
|
||||
Value: {
|
||||
'000001.SH': {
|
||||
Date: ['20260817', '20260818'],
|
||||
Open: ['1', '1'],
|
||||
High: ['1', '1'],
|
||||
Low: ['1', '1'],
|
||||
Close: ['100', '102'],
|
||||
Amount: ['1', '1'],
|
||||
Volume: ['1', '1'],
|
||||
},
|
||||
'399001.SZ': {
|
||||
Date: ['20260818', '20260819'],
|
||||
Open: ['1', '1'],
|
||||
High: ['1', '1'],
|
||||
Low: ['1', '1'],
|
||||
Close: ['100', '101'],
|
||||
Amount: ['1', '1'],
|
||||
Volume: ['1', '1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
[
|
||||
{ code: '000001.SH', name: '旧日期指数' },
|
||||
{ code: '399001.SZ', name: '最新日期指数' },
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toEqual([expect.objectContaining({ code: '399001.SZ', tradeDate: '20260819' })]);
|
||||
});
|
||||
|
||||
it('板块只保留有两个交易日和有效涨跌幅的精确字段', () => {
|
||||
const result = mapSectorQuotes(
|
||||
{
|
||||
@@ -129,6 +178,33 @@ describe('TDX 行情映射', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('板块混合交易日时只保留最新交易日,避免把不同日期混排', () => {
|
||||
const series = (dates: string[], close: string[]) => ({
|
||||
Date: dates,
|
||||
Open: close,
|
||||
High: close,
|
||||
Low: close,
|
||||
Close: close,
|
||||
Amount: ['1', '1'],
|
||||
Volume: ['1', '1'],
|
||||
});
|
||||
expect(
|
||||
mapSectorQuotes(
|
||||
{
|
||||
ErrorId: '0',
|
||||
Value: {
|
||||
'881001.SH': series(['20260817', '20260818'], ['100', '102']),
|
||||
'881002.SH': series(['20260818', '20260819'], ['100', '101']),
|
||||
},
|
||||
},
|
||||
[
|
||||
{ code: '881001.SH', name: '旧日期行业' },
|
||||
{ code: '881002.SH', name: '最新日期行业' },
|
||||
],
|
||||
),
|
||||
).toEqual([expect.objectContaining({ code: '881002.SH', tradeDate: '20260819' })]);
|
||||
});
|
||||
|
||||
it('成交额映射将 TDX 万元转换为亿元并计算环比', () => {
|
||||
expect(
|
||||
mapTurnoverTrend(
|
||||
@@ -193,4 +269,44 @@ describe('TDX 行情映射', () => {
|
||||
data: { currentAmount: 12, previousAmount: 10, marketCount: 1, tradeDate: '20260818' },
|
||||
});
|
||||
});
|
||||
|
||||
it('成交额以多数市场的最新交易日为锚,避免首个市场日期异常导致丢数', () => {
|
||||
const series = (dates: string[], amounts: string[]) => ({
|
||||
Date: dates,
|
||||
Open: ['1', '1'],
|
||||
High: ['1', '1'],
|
||||
Low: ['1', '1'],
|
||||
Close: ['1', '1'],
|
||||
Amount: amounts,
|
||||
Volume: ['1', '1'],
|
||||
});
|
||||
expect(
|
||||
mapTurnoverTrend(
|
||||
{
|
||||
ErrorId: '0',
|
||||
Value: {
|
||||
'000001.SH': series(['20260817', '20260818'], ['100000', '120000']),
|
||||
'399001.SZ': series(['20260818', '20260819'], ['200000', '240000']),
|
||||
'899050.BJ': series(['20260818', '20260819'], ['300000', '360000']),
|
||||
},
|
||||
},
|
||||
[
|
||||
{ code: '000001.SH', name: '上证市场' },
|
||||
{ code: '399001.SZ', name: '深证市场' },
|
||||
{ code: '899050.BJ', name: '北证市场' },
|
||||
],
|
||||
),
|
||||
).toEqual({
|
||||
data: {
|
||||
currentAmount: 60,
|
||||
previousAmount: 50,
|
||||
changePercent: 20,
|
||||
unit: '亿元',
|
||||
marketCount: 2,
|
||||
tradeDate: '20260819',
|
||||
previousTradeDate: '20260818',
|
||||
},
|
||||
validMarketCount: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,24 @@ function toNonNegativeNumber(value: string | undefined): number | null {
|
||||
return parsed !== null && parsed >= 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function toNonNegativeInteger(value: string | undefined): number | null {
|
||||
const normalized = value?.trim();
|
||||
if (!normalized || !/^\d+$/.test(normalized)) return null;
|
||||
const parsed = Number(normalized);
|
||||
return Number.isSafeInteger(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function toTradeDate(value: string | undefined): string | null {
|
||||
return value && /^\d{8}$/.test(value) ? value : null;
|
||||
}
|
||||
|
||||
function latestTradeDate<T extends { tradeDate: string }>(items: readonly T[]): string | null {
|
||||
return items.reduce<string | null>(
|
||||
(latest, item) => (latest === null || item.tradeDate > latest ? item.tradeDate : latest),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
function mapDirection(changePercent: number | null): IndexQuoteDto['direction'] {
|
||||
if (changePercent === null || changePercent === 0) return 'flat';
|
||||
return changePercent > 0 ? 'up' : 'down';
|
||||
@@ -26,7 +44,7 @@ function mapIndexQuote(series: TdxKlineSeries, definition: IndexDefinition): Ind
|
||||
const lastIndex = series.Close.length - 1;
|
||||
const price = toNumber(series.Close[lastIndex]);
|
||||
const previousPrice = toNumber(series.Close[lastIndex - 1]);
|
||||
const tradeDate = series.Date[lastIndex];
|
||||
const tradeDate = toTradeDate(series.Date[lastIndex]);
|
||||
if (price === null || !tradeDate) return null;
|
||||
|
||||
const changePercent =
|
||||
@@ -49,12 +67,14 @@ export function mapIndexQuotes(
|
||||
result: TdxMarketDataResult,
|
||||
definitions: readonly IndexDefinition[],
|
||||
): IndexQuoteDto[] {
|
||||
return definitions.flatMap((definition) => {
|
||||
const candidates = definitions.flatMap((definition) => {
|
||||
const series = result.Value[definition.code];
|
||||
if (!series) return [];
|
||||
const quote = mapIndexQuote(series, definition);
|
||||
return quote ? [quote] : [];
|
||||
});
|
||||
const tradeDate = latestTradeDate(candidates);
|
||||
return tradeDate ? candidates.filter((item) => item.tradeDate === tradeDate) : [];
|
||||
}
|
||||
|
||||
/** TDX 涨跌家数必须成对有效且非负;无效市场整体排除,禁止把缺失字段折算为零。 */
|
||||
@@ -63,8 +83,8 @@ export function mapBreadthSnapshots(snapshots: readonly { UpHome?: string; DownH
|
||||
validCount: number;
|
||||
} {
|
||||
const validSnapshots = snapshots.flatMap((item) => {
|
||||
const advancing = toNonNegativeNumber(item.UpHome);
|
||||
const declining = toNonNegativeNumber(item.DownHome);
|
||||
const advancing = toNonNegativeInteger(item.UpHome);
|
||||
const declining = toNonNegativeInteger(item.DownHome);
|
||||
return advancing === null || declining === null ? [] : [{ advancing, declining }];
|
||||
});
|
||||
if (!validSnapshots.length) return { data: null, validCount: 0 };
|
||||
@@ -84,22 +104,24 @@ export function mapSectorQuotes(
|
||||
result: TdxMarketDataResult,
|
||||
definitions: readonly IndexDefinition[],
|
||||
): SectorQuoteDto[] {
|
||||
return definitions
|
||||
.flatMap((definition) => {
|
||||
const series = result.Value[definition.code];
|
||||
if (!series) return [];
|
||||
const quote = mapIndexQuote(series, definition);
|
||||
if (!quote || quote.changePercent === null) return [];
|
||||
return [
|
||||
{
|
||||
code: quote.code,
|
||||
name: quote.name,
|
||||
changePercent: quote.changePercent,
|
||||
direction: quote.direction,
|
||||
tradeDate: quote.tradeDate,
|
||||
},
|
||||
];
|
||||
})
|
||||
const candidates = definitions.flatMap((definition) => {
|
||||
const series = result.Value[definition.code];
|
||||
if (!series) return [];
|
||||
const quote = mapIndexQuote(series, definition);
|
||||
if (!quote || quote.changePercent === null) return [];
|
||||
return [
|
||||
{
|
||||
code: quote.code,
|
||||
name: quote.name,
|
||||
changePercent: quote.changePercent,
|
||||
direction: quote.direction,
|
||||
tradeDate: quote.tradeDate,
|
||||
},
|
||||
];
|
||||
});
|
||||
const tradeDate = latestTradeDate(candidates);
|
||||
return candidates
|
||||
.filter((item) => item.tradeDate === tradeDate)
|
||||
.sort((left, right) => right.changePercent - left.changePercent);
|
||||
}
|
||||
|
||||
@@ -113,8 +135,8 @@ export function mapTurnoverTrend(
|
||||
if (!series) return [];
|
||||
const current = toNonNegativeNumber(series.Amount.at(-1));
|
||||
const previous = toNonNegativeNumber(series.Amount.at(-2));
|
||||
const tradeDate = series.Date.at(-1);
|
||||
const previousTradeDate = series.Date.at(-2);
|
||||
const tradeDate = toTradeDate(series.Date.at(-1));
|
||||
const previousTradeDate = toTradeDate(series.Date.at(-2));
|
||||
if (
|
||||
current === null ||
|
||||
previous === null ||
|
||||
@@ -129,7 +151,12 @@ export function mapTurnoverTrend(
|
||||
});
|
||||
if (!candidates.length) return { data: null, validMarketCount: 0 };
|
||||
|
||||
const expected = candidates[0];
|
||||
const expected = candidates.reduce((latest, item) =>
|
||||
item.tradeDate > latest.tradeDate ||
|
||||
(item.tradeDate === latest.tradeDate && item.previousTradeDate > latest.previousTradeDate)
|
||||
? item
|
||||
: latest,
|
||||
);
|
||||
const aligned = candidates.filter(
|
||||
(item) =>
|
||||
item.tradeDate === expected.tradeDate &&
|
||||
|
||||
Reference in New Issue
Block a user