parent
8f74e7d4fa
commit
f176610102
@ -0,0 +1,43 @@
|
||||
import { Router } from 'express';
|
||||
import {
|
||||
healthCheck,
|
||||
getStockKLines,
|
||||
getBatchStockKLines,
|
||||
getStockSymbols,
|
||||
getTradingDates,
|
||||
getSourceStatus,
|
||||
switchSource,
|
||||
backfillData,
|
||||
} from '../controllers/customDataSourceController';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ========== 管理接口 ==========
|
||||
|
||||
// 健康检查(无需认证)
|
||||
router.get('/admin/health', healthCheck);
|
||||
|
||||
// 获取数据源状态
|
||||
router.get('/admin/source/status', getSourceStatus);
|
||||
|
||||
// 切换数据源
|
||||
router.post('/admin/source/switch', switchSource);
|
||||
|
||||
// 历史数据补录
|
||||
router.post('/admin/backfill', backfillData);
|
||||
|
||||
// ========== 股票接口 ==========
|
||||
|
||||
// 查询股票K线
|
||||
router.get('/stock/klines/:symbol', getStockKLines);
|
||||
|
||||
// 批量查询股票K线
|
||||
router.post('/stock/klines/batch', getBatchStockKLines);
|
||||
|
||||
// 查询股票列表
|
||||
router.get('/stock/symbols', getStockSymbols);
|
||||
|
||||
// 查询交易日历
|
||||
router.get('/stock/trading-dates', getTradingDates);
|
||||
|
||||
export default router;
|
||||
@ -0,0 +1,269 @@
|
||||
import axios from 'axios';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
// 环境变量配置
|
||||
const EXTERNAL_DATA_SOURCE_URL = process.env.EXTERNAL_DATA_SOURCE_URL || '';
|
||||
const EXTERNAL_DATA_SOURCE_API_KEY = process.env.EXTERNAL_DATA_SOURCE_API_KEY || '';
|
||||
const EXTERNAL_DATA_SOURCE_TIMEOUT = parseInt(process.env.EXTERNAL_DATA_SOURCE_TIMEOUT || '30000', 10);
|
||||
|
||||
// K线数据项
|
||||
export interface KLineItem {
|
||||
date: string;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
// 标的信息
|
||||
export interface SymbolInfo {
|
||||
symbol: string;
|
||||
name: string;
|
||||
type: string;
|
||||
exchange?: string;
|
||||
industry?: string;
|
||||
}
|
||||
|
||||
// 交易日历项
|
||||
export interface TradingDate {
|
||||
date: string;
|
||||
isTrading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部数据源服务
|
||||
* 用于访问第三方数据源服务
|
||||
*/
|
||||
export class ExternalDataSourceService {
|
||||
private enabled: boolean;
|
||||
private baseUrl: string;
|
||||
private apiKey: string;
|
||||
private timeout: number;
|
||||
|
||||
constructor() {
|
||||
this.enabled = !!EXTERNAL_DATA_SOURCE_URL;
|
||||
this.baseUrl = EXTERNAL_DATA_SOURCE_URL;
|
||||
this.apiKey = EXTERNAL_DATA_SOURCE_API_KEY;
|
||||
this.timeout = EXTERNAL_DATA_SOURCE_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求配置
|
||||
*/
|
||||
private getRequestConfig() {
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.apiKey) {
|
||||
headers['X-API-Key'] = this.apiKey;
|
||||
}
|
||||
return {
|
||||
headers,
|
||||
timeout: this.timeout,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
async healthCheck(): Promise<{ status: string; timestamp?: string }> {
|
||||
if (!this.enabled) {
|
||||
return { status: 'disabled' };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${this.baseUrl}/v1/admin/health`,
|
||||
this.getRequestConfig()
|
||||
);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
logger.error('External data source health check failed:', error.message);
|
||||
return { status: 'unhealthy' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询K线数据
|
||||
*/
|
||||
async getKLines(
|
||||
symbol: string,
|
||||
period: 'day' | 'week' | 'month' = 'day',
|
||||
options?: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
limit?: number;
|
||||
}
|
||||
): Promise<KLineItem[]> {
|
||||
if (!this.enabled) {
|
||||
throw new Error('External data source is not enabled');
|
||||
}
|
||||
|
||||
const params: Record<string, any> = {
|
||||
period,
|
||||
};
|
||||
|
||||
if (options?.startDate) params.startDate = options.startDate;
|
||||
if (options?.endDate) params.endDate = options.endDate;
|
||||
if (options?.limit) params.limit = options.limit;
|
||||
|
||||
const response = await axios.get(
|
||||
`${this.baseUrl}/v1/stock/klines/${symbol}`,
|
||||
{ ...this.getRequestConfig(), params }
|
||||
);
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(response.data.message || 'Failed to get KLines');
|
||||
}
|
||||
|
||||
return response.data.data?.items || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询K线数据
|
||||
*/
|
||||
async getBatchKLines(options: {
|
||||
symbols: string[];
|
||||
period?: 'day' | 'week' | 'month';
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
limit?: number;
|
||||
}): Promise<Array<{ symbol: string; data: KLineItem[] }>> {
|
||||
if (!this.enabled) {
|
||||
throw new Error('External data source is not enabled');
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
`${this.baseUrl}/v1/stock/klines/batch`,
|
||||
{
|
||||
symbols: options.symbols,
|
||||
period: options.period || 'day',
|
||||
startDate: options.startDate,
|
||||
endDate: options.endDate,
|
||||
limit: options.limit,
|
||||
},
|
||||
this.getRequestConfig()
|
||||
);
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(response.data.message || 'Failed to get batch KLines');
|
||||
}
|
||||
|
||||
// 转换响应格式
|
||||
const results = response.data.data?.results || [];
|
||||
return results.map((r: any) => ({
|
||||
symbol: r.symbol,
|
||||
data: r.data?.items || [],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询标的信息
|
||||
*/
|
||||
async getSymbols(options?: {
|
||||
type?: 'stock' | 'index' | 'etf' | 'bond';
|
||||
exchange?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<SymbolInfo[]> {
|
||||
if (!this.enabled) {
|
||||
throw new Error('External data source is not enabled');
|
||||
}
|
||||
|
||||
const params: Record<string, any> = {};
|
||||
if (options?.type) params.type = options.type;
|
||||
if (options?.exchange) params.exchange = options.exchange;
|
||||
if (options?.limit) params.limit = options.limit;
|
||||
if (options?.offset) params.offset = options.offset;
|
||||
|
||||
const response = await axios.get(
|
||||
`${this.baseUrl}/v1/stock/symbols`,
|
||||
{ ...this.getRequestConfig(), params }
|
||||
);
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(response.data.message || 'Failed to get symbols');
|
||||
}
|
||||
|
||||
return response.data.data?.items || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询交易日历
|
||||
*/
|
||||
async getTradingDates(startDate: string, endDate: string): Promise<TradingDate[]> {
|
||||
if (!this.enabled) {
|
||||
throw new Error('External data source is not enabled');
|
||||
}
|
||||
|
||||
const response = await axios.get(
|
||||
`${this.baseUrl}/v1/stock/trading-dates`,
|
||||
{
|
||||
...this.getRequestConfig(),
|
||||
params: { startDate, endDate },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(response.data.message || 'Failed to get trading dates');
|
||||
}
|
||||
|
||||
return response.data.data?.items || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近交易日
|
||||
*/
|
||||
async getNearestTradingDate(date?: string): Promise<string | null> {
|
||||
if (!this.enabled) {
|
||||
throw new Error('External data source is not enabled');
|
||||
}
|
||||
|
||||
const params: Record<string, any> = {};
|
||||
if (date) params.date = date;
|
||||
|
||||
const response = await axios.get(
|
||||
`${this.baseUrl}/v1/stock/nearest-trading-date`,
|
||||
{ ...this.getRequestConfig(), params }
|
||||
);
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(response.data.message || 'Failed to get nearest trading date');
|
||||
}
|
||||
|
||||
return response.data.data?.nearestTradingDate || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为交易日
|
||||
*/
|
||||
async isTradingDate(date: string): Promise<boolean> {
|
||||
if (!this.enabled) {
|
||||
throw new Error('External data source is not enabled');
|
||||
}
|
||||
|
||||
const response = await axios.get(
|
||||
`${this.baseUrl}/v1/stock/is-trading-date`,
|
||||
{
|
||||
...this.getRequestConfig(),
|
||||
params: { date },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(response.data.message || 'Failed to check trading date');
|
||||
}
|
||||
|
||||
return response.data.data?.isTrading || false;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const externalDataSourceService = new ExternalDataSourceService();
|
||||
Loading…
Reference in new issue