feat(FEAT-MO-001): 增加市场概览分模块接口

This commit is contained in:
2026-08-18 13:31:24 +08:00
parent c2fa929936
commit 15a25002ce
12 changed files with 834 additions and 1 deletions
@@ -0,0 +1,108 @@
import { Inject, Injectable } from '@nestjs/common';
import type {
BreadthDataDto,
FundDataDto,
IndicesDataDto,
SectorDataDto,
SummaryDataDto,
} from './market-overview.dto';
import { CORE_INDEXES } from './tdx/tdx.types';
import { mapIndexQuotes } from './tdx/tdx.mapper';
import { TdxClientError, type TdxClient } from './tdx/tdx.client';
import type { TdxMarketDataResult } from './tdx/tdx.types';
export const TDX_CLIENT_PORT = Symbol('TDX_CLIENT_PORT');
export type MarketOverviewResponse<TData> = {
snapshotId: string;
asOf: string;
source: 'tdx';
status: 'fresh' | 'delayed' | 'partial' | 'unavailable';
data: TData | null;
warnings: string[];
};
function createSnapshotMeta(): Pick<MarketOverviewResponse<never>, 'snapshotId' | 'asOf' | 'source'> {
const asOf = new Date().toISOString();
const snapshotId = `${asOf.slice(0, 10).replaceAll('-', '')}-${asOf.slice(11, 19).replaceAll(':', '')}`;
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) {}
async getIndices(): Promise<MarketOverviewResponse<IndicesDataDto>> {
const meta = createSnapshotMeta();
try {
const result = await this.tdxClient.request<TdxMarketDataResult>('get_market_data', {
stock_list: CORE_INDEXES.map((item) => item.code),
count: 5,
dividend_type: 'none',
period: '1d',
});
const items = mapIndexQuotes(result, CORE_INDEXES);
const status = items.length === CORE_INDEXES.length ? 'fresh' : 'partial';
return {
...meta,
status,
data: { items },
warnings: status === 'partial' ? ['部分核心指数未返回有效数据'] : [],
};
} catch (error) {
return {
...meta,
status: 'unavailable',
data: null,
warnings: [this.toWarning(error, '核心指数')],
};
}
}
async getSummary(): Promise<MarketOverviewResponse<SummaryDataDto>> {
const indices = await this.getIndices();
if (indices.status === 'unavailable' || !indices.data) {
return { ...indices, data: null };
}
const changes = indices.data.items
.map((item) => item.changePercent)
.filter((value): value is number => value !== null);
const averageChange = changes.length
? Math.round((changes.reduce((sum, value) => sum + value, 0) / changes.length) * 100) / 100
: null;
const marketStatus = averageChange === null ? '指数数据不完整' : averageChange >= 0 ? '指数偏强' : '指数偏弱';
const summary: SummaryDataDto = {
verdict: averageChange === null ? '市场状态待确认' : averageChange >= 0 ? '市场维持偏强震荡' : '市场维持偏弱震荡',
marketStatus,
volumeStatus: '成交量口径待接入',
sentimentStatus: '市场情绪口径待接入',
drivers: changes.length ? [`核心指数平均涨跌幅 ${averageChange}%`] : ['核心指数数据不足'],
risks: ['市场宽度、板块和资金指标尚未由当前 TDX 桥接层提供'],
};
return { ...indices, data: summary };
}
async getBreadth(): Promise<MarketOverviewResponse<BreadthDataDto>> {
return unavailable<BreadthDataDto>('当前 TDX 桥接层未提供市场宽度方法');
}
async getSectors(): Promise<MarketOverviewResponse<SectorDataDto>> {
return unavailable<SectorDataDto>('当前 TDX 桥接层未提供板块排行方法');
}
async getFunds(): Promise<MarketOverviewResponse<FundDataDto>> {
return unavailable<FundDataDto>('当前 TDX 桥接层未提供资金趋势方法');
}
private toWarning(error: unknown, subject: string): string {
if (error instanceof TdxClientError && error.kind === 'timeout') return `${subject}请求超时`;
if (error instanceof TdxClientError && error.kind === 'network') return `${subject}网络不可用`;
return `${subject}数据不可用`;
}
}