feat(FEAT-MO-001): 接入TDX市场宽度板块与成交额趋势

This commit is contained in:
2026-08-18 17:37:36 +08:00
parent 82619489a9
commit ff8ada8316
21 changed files with 616 additions and 69 deletions
@@ -8,9 +8,19 @@ import type {
SummaryDataDto,
} from './market-overview.dto';
import { CORE_INDEXES } from './tdx/tdx.types';
import { mapIndexQuotes } from './tdx/tdx.mapper';
import {
mapBreadthSnapshots,
mapIndexQuotes,
mapSectorQuotes,
mapTurnoverTrend,
} from './tdx/tdx.mapper';
import { TdxClientError, type TdxClient } from './tdx/tdx.client';
import type { TdxMarketDataResult } from './tdx/tdx.types';
import type {
TdxMarketDataResult,
TdxMarketSnapshotResult,
TdxStockListResult,
} from './tdx/tdx.types';
import { BREADTH_INDEXES, TURNOVER_INDEXES } from './tdx/tdx.types';
export const TDX_CLIENT_PORT = Symbol('TDX_CLIENT_PORT');
@@ -32,10 +42,6 @@ function createSnapshotMeta(): Pick<
return { snapshotId, asOf, source: 'tdx' };
}
function unavailable<TData>(warning: string): MarketOverviewResponse<TData> {
return { ...createSnapshotMeta(), status: 'unavailable', data: null, warnings: [warning] };
}
@Injectable()
export class MarketOverviewService {
constructor(@Inject(TDX_CLIENT_PORT) private readonly tdxClient: TdxClient) {}
@@ -89,24 +95,110 @@ export class MarketOverviewService {
? '市场维持偏强震荡'
: '市场维持偏弱震荡',
marketStatus,
volumeStatus: '成交量口径待接入',
sentimentStatus: '市场情绪口径待接入',
volumeStatus: '成交额趋势见下方',
sentimentStatus: '涨跌家数见下方',
drivers: changes.length ? [`核心指数平均涨跌幅 ${averageChange}%`] : ['核心指数数据不足'],
risks: ['市场宽度、板块和资金指标尚未由当前 TDX 桥接层提供'],
risks: ['主力净流入和盘中新闻仍待补充数据源验证'],
};
return { ...indices, data: summary };
}
async getBreadth(): Promise<MarketOverviewResponse<BreadthDataDto>> {
return unavailable<BreadthDataDto>('当前 TDX 桥接层未提供市场宽度方法');
const meta = createSnapshotMeta();
const results = await Promise.allSettled(
BREADTH_INDEXES.map((item) =>
this.tdxClient.request<TdxMarketSnapshotResult>('get_market_snapshot', {
stock_code: item.code,
}),
),
);
const snapshots = results.flatMap((result) =>
result.status === 'fulfilled' ? [result.value] : [],
);
if (!snapshots.length) {
return {
...meta,
status: 'unavailable',
data: null,
warnings: [
this.toWarning(
results[0]?.status === 'rejected' ? results[0].reason : undefined,
'市场宽度',
),
],
};
}
const status = snapshots.length === BREADTH_INDEXES.length ? 'fresh' : 'partial';
return {
...meta,
status,
data: mapBreadthSnapshots(snapshots),
warnings: status === 'partial' ? ['部分市场指数未返回涨跌家数'] : [],
};
}
async getSectors(): Promise<MarketOverviewResponse<SectorDataDto>> {
return unavailable<SectorDataDto>('当前 TDX 桥接层未提供板块排行方法');
const meta = createSnapshotMeta();
try {
const catalog = await this.tdxClient.request<TdxStockListResult>('get_stock_list', {
market: '16',
list_type: 1,
});
const definitions = catalog.Value.map(({ Code: code, Name: name }) => ({ code, name }));
const result = await this.tdxClient.request<TdxMarketDataResult>('get_market_data', {
stock_list: definitions.map((item) => item.code),
count: 2,
dividend_type: 'none',
period: '1d',
});
const mappedItems = mapSectorQuotes(result, definitions);
const items = mappedItems.slice(0, 8);
const status =
mappedItems.length === definitions.length
? 'fresh'
: mappedItems.length
? 'partial'
: 'unavailable';
return {
...meta,
status,
data: items.length ? { items } : null,
warnings:
status === 'fresh'
? []
: [status === 'partial' ? '部分一级行业未返回有效行情' : '一级行业行情不可用'],
};
} catch (error) {
return {
...meta,
status: 'unavailable',
data: null,
warnings: [this.toWarning(error, '板块行情')],
};
}
}
async getFunds(): Promise<MarketOverviewResponse<FundDataDto>> {
return unavailable<FundDataDto>('当前 TDX 桥接层未提供资金趋势方法');
const meta = createSnapshotMeta();
try {
const result = await this.tdxClient.request<TdxMarketDataResult>('get_market_data', {
stock_list: TURNOVER_INDEXES.map((item) => item.code),
count: 2,
dividend_type: 'none',
period: '1d',
});
const data = mapTurnoverTrend(result, TURNOVER_INDEXES);
if (!data)
return { ...meta, status: 'unavailable', data: null, warnings: ['成交额趋势数据不完整'] };
return { ...meta, status: 'fresh', data, warnings: [] };
} catch (error) {
return {
...meta,
status: 'unavailable',
data: null,
warnings: [this.toWarning(error, '成交额趋势')],
};
}
}
private toWarning(error: unknown, subject: string): string {