feat(FEAT-MO-001): 接入TDX市场宽度板块与成交额趋势
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user