feat(FEAT-MO-001): 增加市场概览分模块接口
This commit is contained in:
@@ -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}数据不可用`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user