UNPKG

@gabriel3615/ta_analysis

Version:

stock ta analysis

299 lines (282 loc) 8.49 kB
import type { Candle } from '../../../types.js'; import { calculateATR } from '../../../util/taUtil.js'; import { trendlineConfig } from './trendlineConfig.js'; import type { FittedLine, Channel, BreakoutRetest, TrendlineChannelAnalysisResult, } from './trendlineTypes.js'; function fitLine(points: Array<{ x: number; y: number }>): { slope: number; intercept: number; } { const n = points.length; let sumX = 0, sumY = 0, sumXX = 0, sumXY = 0; for (const p of points) { sumX += p.x; sumY += p.y; sumXX += p.x * p.x; sumXY += p.x * p.y; } const denom = n * sumXX - sumX * sumX || 1e-8; const slope = (n * sumXY - sumX * sumY) / denom; const intercept = (sumY - slope * sumX) / n; return { slope, intercept }; } function countTouches( data: Candle[], line: { slope: number; intercept: number }, kind: 'support' | 'resistance', tol: number, start: number, end: number ): number { let count = 0; for (let i = start; i <= end; i++) { const price = line.slope * i + line.intercept; const ref = kind === 'support' ? data[i].low : data[i].high; if (Math.abs(ref - price) / price <= tol) count++; } return count; } function buildChannelFromSupport( data: Candle[], support: FittedLine ): Channel | undefined { const start = support.startIndex, end = support.endIndex; // 以支持线平移到最高点构建上边界 let maxDelta = -Infinity; for (let i = start; i <= end; i++) { const base = support.slope * i + support.intercept; maxDelta = Math.max(maxDelta, data[i].high - base); } const upper = { ...support, kind: 'resistance' as const, intercept: support.intercept + maxDelta, }; const mid = { slope: support.slope, intercept: support.intercept + maxDelta / 2, startIndex: start, endIndex: end, touches: 0, kind: 'mid' as const, }; const width = maxDelta; return { upper, lower: support, mid, width, slope: mid.slope, touchesUpper: countTouches( data, upper, 'resistance', trendlineConfig.priceTolerancePercent, start, end ), touchesLower: support.touches, }; } function buildChannelFromResistance( data: Candle[], resistance: FittedLine ): Channel | undefined { const start = resistance.startIndex, end = resistance.endIndex; // 以阻力线平移到最低点构建下边界 let maxDelta = -Infinity; for (let i = start; i <= end; i++) { const base = resistance.slope * i + resistance.intercept; maxDelta = Math.max(maxDelta, base - data[i].low); } const lower = { ...resistance, kind: 'support' as const, intercept: resistance.intercept - maxDelta, }; const mid = { slope: resistance.slope, intercept: resistance.intercept - maxDelta / 2, startIndex: start, endIndex: end, touches: 0, kind: 'mid' as const, }; const width = maxDelta; return { upper: resistance, lower, mid, width, slope: mid.slope, touchesUpper: resistance.touches, touchesLower: countTouches( data, lower, 'support', trendlineConfig.priceTolerancePercent, start, end ), }; } function detectBreakoutRetest( data: Candle[], ch: Channel ): BreakoutRetest | undefined { const lastIndex = data.length - 1; const last = data[lastIndex]; const upperNow = ch.upper.slope * lastIndex + ch.upper.intercept; const lowerNow = ch.lower.slope * lastIndex + ch.lower.intercept; const brokeUp = last.close > upperNow * (1 + trendlineConfig.breakoutThresholdPercent); const brokeDown = last.close < lowerNow * (1 - trendlineConfig.breakoutThresholdPercent); if (!brokeUp && !brokeDown) return; const dir = brokeUp ? ('up' as const) : ('down' as const); // 回踩窗口 const start = Math.max(0, lastIndex - trendlineConfig.retestWindowBars + 1); let retested = false; let retestIndex: number | undefined = undefined; for (let i = start; i <= lastIndex; i++) { const bound = dir === 'up' ? ch.upper.slope * i + ch.upper.intercept : ch.lower.slope * i + ch.lower.intercept; const tol = bound * trendlineConfig.retestTolerancePercent; if (dir === 'up') { if (data[i].low <= bound + tol) { retested = true; retestIndex = i; break; } } else { if (data[i].high >= bound - tol) { retested = true; retestIndex = i; break; } } } // 简单质量评分:触达回踩 + 斜率显著性 + 通道宽度合理性 const atr = calculateATR(data, Math.min(14, data.length)); const slopeScore = Math.min(100, Math.abs(ch.slope) * 1e4); const widthAtr = ch.width / Math.max(1e-8, atr); const widthScore = widthAtr >= trendlineConfig.channelWidthAtrMin && widthAtr <= trendlineConfig.channelWidthAtrMax ? 100 : 40; const retestScore = retested ? 100 : 40; const qualityScore = Math.round( trendlineConfig.scoreWeights.slope * (slopeScore / 100) + trendlineConfig.scoreWeights.retest * (retestScore / 100) + trendlineConfig.scoreWeights.touches * ((ch.touchesUpper + ch.touchesLower) / 6) + trendlineConfig.scoreWeights.follow * (widthScore / 100) ); return { direction: dir, breakoutIndex: lastIndex, retested, retestIndex, qualityScore, }; } export function analyzeTrendlinesAndChannels( symbol: string, data: Candle[], timeframe: 'weekly' | 'daily' | '1hour' ): TrendlineChannelAnalysisResult { const n = data.length; const start = Math.max(0, n - trendlineConfig.maxLookbackBars); const pointsSupport: Array<{ x: number; y: number }> = []; const pointsResistance: Array<{ x: number; y: number }> = []; for (let i = start; i < n; i++) { pointsSupport.push({ x: i, y: data[i].low }); pointsResistance.push({ x: i, y: data[i].high }); } const sup = fitLine(pointsSupport); const res = fitLine(pointsResistance); const tol = trendlineConfig.priceTolerancePercent; const supTouches = countTouches(data, sup, 'support', tol, start, n - 1); const resTouches = countTouches(data, res, 'resistance', tol, start, n - 1); const fittedSupport: FittedLine | undefined = Math.abs(sup.slope) >= trendlineConfig.minSlopeAbs && supTouches >= trendlineConfig.minTouches ? { slope: sup.slope, intercept: sup.intercept, startIndex: start, endIndex: n - 1, touches: supTouches, kind: 'support', } : undefined; const fittedResistance: FittedLine | undefined = Math.abs(res.slope) >= trendlineConfig.minSlopeAbs && resTouches >= trendlineConfig.minTouches ? { slope: res.slope, intercept: res.intercept, startIndex: start, endIndex: n - 1, touches: resTouches, kind: 'resistance', } : undefined; // 分别从支撑线和阻力线独立构建通道 let channelFromSupport: Channel | undefined = undefined; let channelFromResistance: Channel | undefined = undefined; if (fittedSupport) { channelFromSupport = buildChannelFromSupport(data, fittedSupport); } if (fittedResistance) { channelFromResistance = buildChannelFromResistance(data, fittedResistance); } // 选择质量更好的通道(触达数优先),如都无则为undefined let channel: Channel | undefined = undefined; if (channelFromSupport && channelFromResistance) { const supportTouches = channelFromSupport.touchesUpper + channelFromSupport.touchesLower; const resistanceTouches = channelFromResistance.touchesUpper + channelFromResistance.touchesLower; channel = supportTouches >= resistanceTouches ? channelFromSupport : channelFromResistance; } else { channel = channelFromSupport || channelFromResistance; } const breakoutRetest = channel ? detectBreakoutRetest(data, channel) : undefined; const slopeStr = channel ? channel.slope > 0 ? '向上' : channel.slope < 0 ? '向下' : '水平' : '—'; const summary = channel ? `通道斜率: ${slopeStr} | 宽度: ${channel.width.toFixed(2)} | 上触达:${channel.touchesUpper} 下触达:${channel.touchesLower}` : '未形成稳定通道'; return { symbol, timeframe, fittedSupport, fittedResistance, channel, breakoutRetest, summary, }; }