|
|
@@ -9,43 +9,108 @@ const log = {
|
|
|
db: debug('backend:db:stock'),
|
|
|
};
|
|
|
|
|
|
+interface StockPriceData {
|
|
|
+ t: string; // 交易时间
|
|
|
+ o: number; // 开盘价
|
|
|
+ h: number; // 最高价
|
|
|
+ l: number; // 最低价
|
|
|
+ c: number; // 收盘价
|
|
|
+ v: number; // 成交量
|
|
|
+ a: number; // 成交额
|
|
|
+ pc: number; // 前收盘价
|
|
|
+ sf: number; // 停牌状态
|
|
|
+}
|
|
|
+
|
|
|
+interface StockStopPrice {
|
|
|
+ t: string; // 交易日期
|
|
|
+ h: number; // 涨停价格
|
|
|
+ l: number; // 跌停价格
|
|
|
+}
|
|
|
+
|
|
|
+interface StockIndicator {
|
|
|
+ time: string; // 更新时间
|
|
|
+ lb: number; // 量比
|
|
|
+ om: number; // 1分钟涨速(%)
|
|
|
+ fm: number; // 5分钟涨速(%)
|
|
|
+ '3d': number; // 3日涨幅(%)
|
|
|
+ '5d': number; // 5日涨幅(%)
|
|
|
+ '10d': number; // 10日涨幅(%)
|
|
|
+ '3t': number; // 3日换手(%)
|
|
|
+ '5t': number; // 5日换手(%)
|
|
|
+ '10t': number; // 10日换手(%)
|
|
|
+}
|
|
|
+
|
|
|
export class StockDataService extends GenericCrudService<StockData> {
|
|
|
constructor(dataSource: DataSource) {
|
|
|
super(dataSource, StockData);
|
|
|
}
|
|
|
|
|
|
+ private getApiBaseUrl(): string {
|
|
|
+ return 'https://api.mairuiapi.com/hsstock';
|
|
|
+ }
|
|
|
+
|
|
|
+ private getLicense(): string {
|
|
|
+ const license = process.env.STOCK_API_LICENSE;
|
|
|
+ if (!license) {
|
|
|
+ throw new Error('STOCK_API_LICENSE environment variable not set');
|
|
|
+ }
|
|
|
+ return license;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取股票历史数据
|
|
|
* 优先从数据库获取,如果没有则调用外部API
|
|
|
+ * 使用新的股票API接口获取历史分时交易数据
|
|
|
* @param code 股票代码
|
|
|
+ * @param market 市场代码(如SZ、SH)
|
|
|
+ * @param level 分时级别(d日线、w周线、m月线、y年线、5/15/30/60分钟)
|
|
|
+ * @param startDate 开始时间(YYYYMMDD格式)
|
|
|
+ * @param endDate 结束时间(YYYYMMDD格式)
|
|
|
+ * @param limit 获取数据条数
|
|
|
* @returns 股票历史数据
|
|
|
*/
|
|
|
- async getStockHistory(code: string = '001339'): Promise<any> {
|
|
|
+ async getStockHistory(
|
|
|
+ code: string,
|
|
|
+ market: string = 'SZ',
|
|
|
+ level: string = 'd',
|
|
|
+ startDate?: string,
|
|
|
+ endDate?: string,
|
|
|
+ limit?: number
|
|
|
+ ): Promise<StockPriceData[]> {
|
|
|
try {
|
|
|
+ // 构建缓存键
|
|
|
+ const cacheKey = `${code}.${market}_${level}_${startDate || ''}_${endDate || ''}_${limit || ''}`;
|
|
|
+
|
|
|
// 查询数据库中是否存在今天的数据
|
|
|
const today = dayjs().format('YYYY-MM-DD');
|
|
|
const existingData = await this.repository
|
|
|
.createQueryBuilder('stock')
|
|
|
- .where('stock.code = :code', { code })
|
|
|
+ .where('stock.code = :cacheKey', { cacheKey })
|
|
|
.andWhere('stock.updated_at >= :today', { today: `${today} 00:00:00` })
|
|
|
.getOne();
|
|
|
|
|
|
if (existingData) {
|
|
|
- log.db(`Found existing data for ${code} on ${today}`);
|
|
|
- return existingData.data;
|
|
|
+ log.db(`Found existing data for ${cacheKey} on ${today}`);
|
|
|
+ return existingData.data as StockPriceData[];
|
|
|
}
|
|
|
|
|
|
// 如果没有今天的数据,调用外部API
|
|
|
- log.api(`Fetching fresh data for ${code} from external API`);
|
|
|
- const dh = 'dn'; // 固定值
|
|
|
+ log.api(`Fetching fresh data for ${cacheKey} from external API`);
|
|
|
+ const license = this.getLicense();
|
|
|
+ let apiUrl = `${this.getApiBaseUrl()}/history/${code}.${market}/${level}/n/${license}`;
|
|
|
+
|
|
|
+ const params = new URLSearchParams();
|
|
|
+ if (startDate) params.append('st', startDate);
|
|
|
+ if (endDate) params.append('et', endDate);
|
|
|
+ if (limit) params.append('lt', limit.toString());
|
|
|
|
|
|
- const license = process.env.STOCK_API_LICENSE;
|
|
|
- if (!license) {
|
|
|
- throw new Error('STOCK_API_LICENSE environment variable not set');
|
|
|
+ const queryString = params.toString();
|
|
|
+ if (queryString) {
|
|
|
+ apiUrl += `?${queryString}`;
|
|
|
}
|
|
|
|
|
|
- const apiUrl = `http://api.mairui.club/hszbl/fsjy/${code}/${dh}/${license}`;
|
|
|
- console.log('apiUrl', apiUrl)
|
|
|
+ log.api(`Fetching stock history from: ${apiUrl}`);
|
|
|
+
|
|
|
const response = await fetch(apiUrl, {
|
|
|
method: 'GET',
|
|
|
headers: {
|
|
|
@@ -57,11 +122,11 @@ export class StockDataService extends GenericCrudService<StockData> {
|
|
|
throw new Error(`API request failed with status ${response.status}`);
|
|
|
}
|
|
|
|
|
|
- const newData = await response.json();
|
|
|
-
|
|
|
+ const newData: StockPriceData[] = await response.json();
|
|
|
+
|
|
|
// 更新或插入数据库
|
|
|
const stockData = new StockData();
|
|
|
- stockData.code = code;
|
|
|
+ stockData.code = cacheKey;
|
|
|
stockData.data = newData;
|
|
|
|
|
|
await this.repository
|
|
|
@@ -74,9 +139,8 @@ export class StockDataService extends GenericCrudService<StockData> {
|
|
|
)
|
|
|
.execute();
|
|
|
|
|
|
- log.api(`Successfully saved fresh data for ${code}`);
|
|
|
+ log.api(`Successfully saved fresh data for ${cacheKey}`);
|
|
|
return newData;
|
|
|
-
|
|
|
} catch (error) {
|
|
|
log.api('Error getting stock history:', error);
|
|
|
throw error;
|
|
|
@@ -85,10 +149,186 @@ export class StockDataService extends GenericCrudService<StockData> {
|
|
|
|
|
|
/**
|
|
|
* 获取股票最新数据
|
|
|
+ * 优先从数据库获取,如果没有则调用外部API
|
|
|
+ * 使用新的股票API接口获取最新分时交易数据
|
|
|
* @param code 股票代码
|
|
|
+ * @param market 市场代码(如SZ、SH)
|
|
|
+ * @param level 分时级别(d日线、w周线、m月线、y年线、5/15/30/60分钟)
|
|
|
+ * @param limit 获取数据条数
|
|
|
* @returns 股票最新数据
|
|
|
*/
|
|
|
- async getLatestStockData(code: string): Promise<StockData | null> {
|
|
|
+ async getLatestStockData(
|
|
|
+ code: string,
|
|
|
+ market: string = 'SZ',
|
|
|
+ level: string = 'd',
|
|
|
+ limit: number = 1
|
|
|
+ ): Promise<StockPriceData[]> {
|
|
|
+ try {
|
|
|
+ // 构建缓存键
|
|
|
+ const cacheKey = `latest_${code}.${market}_${level}_${limit}`;
|
|
|
+
|
|
|
+ // 查询数据库中是否存在今天的数据
|
|
|
+ const today = dayjs().format('YYYY-MM-DD');
|
|
|
+ const existingData = await this.repository
|
|
|
+ .createQueryBuilder('stock')
|
|
|
+ .where('stock.code = :cacheKey', { cacheKey })
|
|
|
+ .andWhere('stock.updated_at >= :today', { today: `${today} 00:00:00` })
|
|
|
+ .getOne();
|
|
|
+
|
|
|
+ if (existingData) {
|
|
|
+ log.db(`Found existing latest data for ${cacheKey} on ${today}`);
|
|
|
+ return existingData.data as StockPriceData[];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有今天的数据,调用外部API
|
|
|
+ log.api(`Fetching fresh latest data for ${cacheKey} from external API`);
|
|
|
+ const license = this.getLicense();
|
|
|
+ const apiUrl = `${this.getApiBaseUrl()}/latest/${code}.${market}/${level}/n/${license}?lt=${limit}`;
|
|
|
+
|
|
|
+ log.api(`Fetching latest stock data from: ${apiUrl}`);
|
|
|
+
|
|
|
+ const response = await fetch(apiUrl, {
|
|
|
+ method: 'GET',
|
|
|
+ headers: {
|
|
|
+ 'Accept': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`API request failed with status ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const newData: StockPriceData[] = await response.json();
|
|
|
+
|
|
|
+ // 更新或插入数据库
|
|
|
+ const stockData = new StockData();
|
|
|
+ stockData.code = cacheKey;
|
|
|
+ stockData.data = newData;
|
|
|
+
|
|
|
+ await this.repository
|
|
|
+ .createQueryBuilder()
|
|
|
+ .insert()
|
|
|
+ .values(stockData)
|
|
|
+ .orUpdate(
|
|
|
+ ['data', 'updated_at'],
|
|
|
+ ['code']
|
|
|
+ )
|
|
|
+ .execute();
|
|
|
+
|
|
|
+ log.api(`Successfully saved fresh latest data for ${cacheKey}`);
|
|
|
+ return newData;
|
|
|
+ } catch (error) {
|
|
|
+ log.api('Error getting latest stock data:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取股票历史涨跌停价格
|
|
|
+ * @param code 股票代码
|
|
|
+ * @param market 市场代码(如SZ、SH)
|
|
|
+ * @param startDate 开始时间(YYYYMMDD格式)
|
|
|
+ * @param endDate 结束时间(YYYYMMDD格式)
|
|
|
+ * @returns 涨跌停价格数据
|
|
|
+ */
|
|
|
+ async getStockStopPrice(
|
|
|
+ code: string,
|
|
|
+ market: string = 'SZ',
|
|
|
+ startDate?: string,
|
|
|
+ endDate?: string
|
|
|
+ ): Promise<StockStopPrice[]> {
|
|
|
+ try {
|
|
|
+ const license = this.getLicense();
|
|
|
+ let apiUrl = `${this.getApiBaseUrl()}/stopprice/history/${code}.${market}/${license}`;
|
|
|
+
|
|
|
+ const params = new URLSearchParams();
|
|
|
+ if (startDate) params.append('st', startDate);
|
|
|
+ if (endDate) params.append('et', endDate);
|
|
|
+
|
|
|
+ const queryString = params.toString();
|
|
|
+ if (queryString) {
|
|
|
+ apiUrl += `?${queryString}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.api(`Fetching stock stop price from: ${apiUrl}`);
|
|
|
+
|
|
|
+ const response = await fetch(apiUrl, {
|
|
|
+ method: 'GET',
|
|
|
+ headers: {
|
|
|
+ 'Accept': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`API request failed with status ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const data: StockStopPrice[] = await response.json();
|
|
|
+ log.api(`Successfully fetched ${data.length} stop price records for ${code}.${market}`);
|
|
|
+
|
|
|
+ return data;
|
|
|
+ } catch (error) {
|
|
|
+ log.api('Error getting stock stop price:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取股票行情指标
|
|
|
+ * @param code 股票代码
|
|
|
+ * @param market 市场代码(如SZ、SH)
|
|
|
+ * @param startDate 开始时间(YYYYMMDD格式)
|
|
|
+ * @param endDate 结束时间(YYYYMMDD格式)
|
|
|
+ * @returns 行情指标数据
|
|
|
+ */
|
|
|
+ async getStockIndicators(
|
|
|
+ code: string,
|
|
|
+ market: string = 'SZ',
|
|
|
+ startDate?: string,
|
|
|
+ endDate?: string
|
|
|
+ ): Promise<StockIndicator[]> {
|
|
|
+ try {
|
|
|
+ const license = this.getLicense();
|
|
|
+ let apiUrl = `${this.getApiBaseUrl()}/indicators/${code}.${market}/${license}`;
|
|
|
+
|
|
|
+ const params = new URLSearchParams();
|
|
|
+ if (startDate) params.append('st', startDate);
|
|
|
+ if (endDate) params.append('et', endDate);
|
|
|
+
|
|
|
+ const queryString = params.toString();
|
|
|
+ if (queryString) {
|
|
|
+ apiUrl += `?${queryString}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.api(`Fetching stock indicators from: ${apiUrl}`);
|
|
|
+
|
|
|
+ const response = await fetch(apiUrl, {
|
|
|
+ method: 'GET',
|
|
|
+ headers: {
|
|
|
+ 'Accept': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`API request failed with status ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const data: StockIndicator[] = await response.json();
|
|
|
+ log.api(`Successfully fetched ${data.length} indicator records for ${code}.${market}`);
|
|
|
+
|
|
|
+ return data;
|
|
|
+ } catch (error) {
|
|
|
+ log.api('Error getting stock indicators:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取股票最新数据(兼容旧方法)
|
|
|
+ * @param code 股票代码
|
|
|
+ * @returns 股票最新数据
|
|
|
+ */
|
|
|
+ async getLatestStockDataOld(code: string): Promise<StockData | null> {
|
|
|
try {
|
|
|
const stockData = await this.repository
|
|
|
.createQueryBuilder('stock')
|
|
|
@@ -104,7 +344,7 @@ export class StockDataService extends GenericCrudService<StockData> {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取多个股票的历史数据
|
|
|
+ * 获取多个股票的历史数据(兼容旧方法)
|
|
|
* @param codes 股票代码数组
|
|
|
* @returns 股票历史数据映射
|
|
|
*/
|
|
|
@@ -113,7 +353,8 @@ export class StockDataService extends GenericCrudService<StockData> {
|
|
|
|
|
|
for (const code of codes) {
|
|
|
try {
|
|
|
- results[code] = await this.getStockHistory(code);
|
|
|
+ const [stockCode, market] = code.includes('.') ? code.split('.') : [code, 'SZ'];
|
|
|
+ results[code] = await this.getStockHistory(stockCode, market);
|
|
|
} catch (error) {
|
|
|
log.api(`Error fetching data for ${code}:`, error);
|
|
|
results[code] = null;
|
|
|
@@ -122,4 +363,39 @@ export class StockDataService extends GenericCrudService<StockData> {
|
|
|
|
|
|
return results;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取股票完整信息
|
|
|
+ * @param code 股票代码
|
|
|
+ * @param market 市场代码
|
|
|
+ * @returns 包含历史数据、最新数据、涨跌停价格和指标的完整信息
|
|
|
+ */
|
|
|
+ async getStockCompleteInfo(
|
|
|
+ code: string,
|
|
|
+ market: string = 'SZ'
|
|
|
+ ): Promise<{
|
|
|
+ history: StockPriceData[];
|
|
|
+ latest: StockPriceData[];
|
|
|
+ stopPrice: StockStopPrice[];
|
|
|
+ indicators: StockIndicator[];
|
|
|
+ }> {
|
|
|
+ try {
|
|
|
+ const [history, latest, stopPrice, indicators] = await Promise.all([
|
|
|
+ this.getStockHistory(code, market),
|
|
|
+ this.getLatestStockData(code, market),
|
|
|
+ this.getStockStopPrice(code, market),
|
|
|
+ this.getStockIndicators(code, market)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return {
|
|
|
+ history,
|
|
|
+ latest,
|
|
|
+ stopPrice,
|
|
|
+ indicators
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ log.api('Error getting stock complete info:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|