UNPKG

@gabriel3615/ta_analysis

Version:

stock ta analysis

257 lines (256 loc) 8.85 kB
import yahooFinance from 'yahoo-finance2'; import { PatternStatus } from '../analysis/basic/patterns/analyzeMultiTimeframePatterns.js'; import { globalLogger } from './logger.js'; export function isToday(date) { const today = new Date(); return (date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate()); } /** * 生成唯一标识符 */ export function generateUniqueId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } export const promiseWithTimeout = async (namedPromise, timeout, errorMsg) => { let timeoutId = undefined; try { const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`${namedPromise.name} ${errorMsg}`)); }, timeout); }); return await Promise.race([ namedPromise.promise.then(r => { return r; }), timeoutPromise, ]); } finally { clearTimeout(timeoutId); } }; export function calculateRSI(prices, period) { const rsi = []; let gains = 0; let losses = 0; for (let i = 1; i <= period; i++) { const change = prices[i] - prices[i - 1]; if (change > 0) { gains += change; } else { losses -= change; } } let averageGain = gains / period; let averageLoss = losses / period; rsi.push(100 - 100 / (1 + averageGain / averageLoss)); for (let i = period + 1; i < prices.length; i++) { const change = prices[i] - prices[i - 1]; if (change > 0) { averageGain = (averageGain * (period - 1) + change) / period; averageLoss = (averageLoss * (period - 1)) / period; } else { averageGain = (averageGain * (period - 1)) / period; averageLoss = (averageLoss * (period - 1) - change) / period; } rsi.push(100 - 100 / (1 + averageGain / averageLoss)); } return rsi; } export function rollingMin(prices, window) { const result = []; for (let i = 0; i < prices.length; i++) { const start = Math.max(0, i - window + 1); const end = i + 1; const windowSlice = prices.slice(start, end); result.push(Math.min(...windowSlice)); } return result; } export function rollingMax(prices, window) { const result = []; for (let i = 0; i < prices.length; i++) { const start = Math.max(0, i - window + 1); const end = i + 1; const windowSlice = prices.slice(start, end); result.push(Math.max(...windowSlice)); } return result; } export function standardDeviation(values) { const mean = values.reduce((sum, value) => sum + value, 0) / values.length; const squaredDiffs = values.map(value => Math.pow(value - mean, 2)); const avgSquaredDiff = squaredDiffs.reduce((sum, value) => sum + value, 0) / values.length; return Math.sqrt(avgSquaredDiff); } export function percentChange(prices) { const changes = []; for (let i = 1; i < prices.length; i++) { const change = ((prices[i] - prices[i - 1]) / prices[i - 1]) * 100; changes.push(change); } return changes; } // same as MarketQuery.getHistoricalData export async function getStockData(symbol, startDate, endDate, interval = '1d') { const queryOptions = { period1: startDate, period2: endDate, interval, }; try { const result = await yahooFinance.chart(symbol, queryOptions); const candles = []; if (result && result.quotes && result.quotes.length > 0) { result.quotes.forEach(quote => { if (quote.date && quote.close && quote.volume !== undefined) { candles.push({ symbol, open: quote.open || quote.close, high: quote.high || quote.close, low: quote.low || quote.close, close: quote.close, volume: quote.volume, timestamp: new Date(quote.date), }); } }); } return candles; } catch (error) { globalLogger.error('获取股票数据时出错:', error); return []; } } export async function getStockDataForTimeframe(symbol, startDate, endDate, timeframe) { // 实际应用中,应该直接从数据提供商获取对应时间周期的数据 // 这里为了简化,我们从日线数据模拟其他时间周期 // 首先获取原始日线数据 const rawData = await getStockData(symbol, startDate, endDate); if (timeframe === 'daily') { return await getStockData(symbol, startDate, endDate); // 直接返回日线数据 } else if (timeframe === 'weekly') { // 将日线数据聚合为周线 return await getStockData(symbol, startDate, endDate, '1wk'); } else if (timeframe === '1hour') { // 注意:实际应用中应该直接获取真实的日内数据 // 过滤掉夜盘影响,成交量为0的数据 return (await getStockData(symbol, startDate, endDate, '1h')).filter(c => c.volume !== 0); } // 默认返回日线数据 return rawData; } /** * 获取形态状态描述 */ export function getStatusDescription(status) { switch (status) { case PatternStatus.Forming: return '正在形成中'; case PatternStatus.Completed: return '已完成但未突破'; case PatternStatus.Confirmed: return '已确认突破'; case PatternStatus.Failed: return '形成后失败'; default: return '未知状态'; } } /** * 将日期转换为美东时间字符串 * @param date */ export function toEDTString(date) { return date.toLocaleString('en-US', { timeZone: 'America/New_York', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }); } /** * Get full exchange name from symbol * @param symbol */ export async function getFullExchangeNameBySymbol(symbol) { try { const result = await yahooFinance.quote(symbol, { fields: ['fullExchangeName'], }); if (result) { let fullExchangeName = result.fullExchangeName; // 返回 fullExchangeName fullExchangeName = fullExchangeName.toLowerCase().includes('nasdaq') ? 'NASDAQ' : fullExchangeName; return fullExchangeName; // 返回 fullExchangeName } else { globalLogger.log(`Can't get ${symbol} full exchange name`); return ''; // 返回 null 表示未找到 } } catch (e) { globalLogger.error(`Error occurs when getting ${symbol} full exchange name`, e.message); return ''; // 返回 null 表示出错 } } /** * Aggregate daily candles into weekly candles (Mon-Sun window) */ export function aggregateDailyToWeekly(daily) { if (!daily.length) return []; // Group by ISO week (year-week) const weekKey = (d) => { const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); // ISO week calculation // Thursday in current week decides the year date.setUTCDate(date.getUTCDate() + 4 - (date.getUTCDay() || 7)); const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); const weekNo = Math.ceil(((+date - +yearStart) / 86400000 + 1) / 7); return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`; }; const groups = new Map(); for (const c of daily) { const key = weekKey(c.timestamp); const arr = groups.get(key); if (arr) arr.push(c); else groups.set(key, [c]); } const result = []; for (const [, arr] of groups) { // sort by time within week arr.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); const open = arr[0].open; const close = arr[arr.length - 1].close; const high = Math.max(...arr.map(x => x.high)); const low = Math.min(...arr.map(x => x.low)); const volume = arr.reduce((sum, x) => sum + (x.volume ?? 0), 0); const timestamp = arr[arr.length - 1].timestamp; // end of week result.push({ symbol: arr[0].symbol, open, high, low, close, volume, timestamp, }); } // Ensure ascending weekly order result.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); return result; }