UNPKG

@gabriel3615/ta_analysis

Version:

stock ta analysis

479 lines (415 loc) 17.2 kB
import { Candle } from '../../../types.js'; import { PatternAnalysisResult, PatternDirection, PatternStatus, PatternType, PeakValley, } from './analyzeMultiTimeframePatterns.js'; import { getStatusDescription } from '../../../util/util.js'; /** * 寻找楔形形态 */ export function findWedges( data: Candle[], peaksValleys: PeakValley[], lookbackPeriod: number = 30 ): PatternAnalysisResult[] { const patterns: PatternAnalysisResult[] = []; // 过滤出最近的峰谷 const recentPoints = peaksValleys.filter( p => p.index >= data.length - lookbackPeriod ); // 排序,确保按时间顺序 recentPoints.sort((a, b) => a.index - b.index); // 分别获取峰和谷 const peaks = recentPoints.filter(p => p.type === 'peak'); const valleys = recentPoints.filter(p => p.type === 'valley'); // 需要至少2个峰和2个谷来形成楔形 if (peaks.length < 2 || valleys.length < 2) { return patterns; } // 寻找上升楔形(看跌) // 上升楔形:上部边界和下部边界都向上倾斜,但下部边界倾斜更陡 if (peaks.length >= 2 && valleys.length >= 2) { const peakSlope = (peaks[peaks.length - 1].price - peaks[peaks.length - 2].price) / (peaks[peaks.length - 1].index - peaks[peaks.length - 2].index); const valleySlope = (valleys[valleys.length - 1].price - valleys[valleys.length - 2].price) / (valleys[valleys.length - 1].index - valleys[valleys.length - 2].index); // 检查两条边界线是否都向上倾斜,且下部边界倾斜更陡 if (peakSlope > 0 && valleySlope > 0 && valleySlope > peakSlope) { // 计算上升楔形的上部边界 const upperBoundary1 = peaks[peaks.length - 2]; const upperBoundary2 = peaks[peaks.length - 1]; // 计算上升楔形的下部边界 const lowerBoundary1 = valleys[valleys.length - 2]; const lowerBoundary2 = valleys[valleys.length - 1]; // 确定形态开始和结束的索引 const startIndex = Math.min(upperBoundary1.index, lowerBoundary1.index); const endIndex = Math.max(upperBoundary2.index, lowerBoundary2.index); // 计算当前的预期边界位置 const currentIndex = data.length - 1; const projectedUpperBoundary = upperBoundary1.price + peakSlope * (currentIndex - upperBoundary1.index); const projectedLowerBoundary = lowerBoundary1.price + valleySlope * (currentIndex - lowerBoundary1.index); // 计算收敛点 let convergenceIndex = upperBoundary1.index; if (peakSlope !== valleySlope) { const interceptDiff = lowerBoundary1.price - upperBoundary1.price - (lowerBoundary1.index - upperBoundary1.index) * (valleySlope - peakSlope); convergenceIndex = upperBoundary1.index + interceptDiff / (valleySlope - peakSlope); } // 计算当前是否接近收敛点 const proximityToConvergence = 1 - Math.min(1, Math.abs(currentIndex - convergenceIndex) / 20); // 确定当前价格相对于楔形的位置 const currentPrice = data[data.length - 1].close; let status = PatternStatus.Forming; if (currentIndex > endIndex) { if (currentPrice < projectedLowerBoundary) { status = PatternStatus.Confirmed; // 已突破下方 } else if (currentPrice > projectedUpperBoundary) { status = PatternStatus.Failed; // 意外向上突破 } else { status = PatternStatus.Completed; // 形成但未突破 } } // 检查形态是否无效 - 上升楔形(看跌)形态 let isInvalid = false; // 检查1:如果之前确认了下方突破,后续K线又回到楔形内部或突破上边界,形态无效 if (status === PatternStatus.Confirmed && endIndex < data.length - 1) { let foundBreakout = false; let invalidAfterBreakout = false; for (let i = endIndex + 1; i < data.length; i++) { // 对于当前K线,计算预期的上下边界 const upperBound = upperBoundary1.price + peakSlope * (i - upperBoundary1.index); const lowerBound = lowerBoundary1.price + valleySlope * (i - lowerBoundary1.index); // 首先检测是否出现下方突破(形态确认) if (!foundBreakout && data[i].close < lowerBound) { foundBreakout = true; continue; } // 如果已经确认下方突破,但后续K线回到楔形内部或突破上边界,形态无效 if ( foundBreakout && (data[i].close > lowerBound || data[i].high > upperBound) ) { invalidAfterBreakout = true; break; } } isInvalid = invalidAfterBreakout; } // 检查2:如果价格长时间在楔形内部盘整而没有突破,可能形态无效 if ( !isInvalid && currentIndex - endIndex > 15 && status === PatternStatus.Completed ) { isInvalid = true; // 形态完成后15个K线仍未突破,考虑无效 } // 如果形态无效,跳过此形态 if (isInvalid) { // 跳过此形态,不添加到结果中 // continue 在这里没有效果,因为我们不在循环内,所以直接使用 if-else 结构 } else { // 计算形态高度(在结束点的高度) const patternHeight = projectedUpperBoundary - projectedLowerBoundary; // 计算价格目标(向下突破的目标,通常是形态起始点的价格) const startHeight = upperBoundary1.price - lowerBoundary1.price; const priceTarget = lowerBoundary1.price - startHeight; // 计算可靠性分数 const reliability = calculateWedgeReliability( data, startIndex, endIndex, patternHeight, proximityToConvergence, status === PatternStatus.Confirmed, PatternType.RisingWedge ); patterns.push({ patternType: PatternType.RisingWedge, status, direction: PatternDirection.Bearish, reliability, significance: reliability * (patternHeight / currentPrice), component: { startIndex, endIndex, keyPoints: [ upperBoundary1, upperBoundary2, lowerBoundary1, lowerBoundary2, ], patternHeight, breakoutLevel: projectedLowerBoundary, volumePattern: analyzeWedgeVolume( data, startIndex, endIndex, status ), }, priceTarget, stopLoss: projectedUpperBoundary * 1.02, // 上边界上方2% breakoutExpected: status === PatternStatus.Completed && proximityToConvergence > 0.7, breakoutDirection: PatternDirection.Bearish, probableBreakoutZone: [ projectedLowerBoundary * 0.98, projectedLowerBoundary * 1.02, ], description: `上升楔形, ${getStatusDescription(status)}, 上边界当前在 ${projectedUpperBoundary.toFixed(2)}, 下边界当前在 ${projectedLowerBoundary.toFixed(2)}`, tradingImplication: `看跌信号, 目标价位: ${priceTarget.toFixed(2)}, 止损位: ${(projectedUpperBoundary * 1.02).toFixed(2)}`, keyDates: [...peaks.map(p => p.date), ...valleys.map(v => v.date)], keyPrices: [...peaks.map(p => p.price), ...valleys.map(v => v.price)], }); } } } // 寻找下降楔形(看涨) // 下降楔形:上部边界和下部边界都向下倾斜,但下部边界倾斜更缓 if (peaks.length >= 2 && valleys.length >= 2) { const peakSlope = (peaks[peaks.length - 1].price - peaks[peaks.length - 2].price) / (peaks[peaks.length - 1].index - peaks[peaks.length - 2].index); const valleySlope = (valleys[valleys.length - 1].price - valleys[valleys.length - 2].price) / (valleys[valleys.length - 1].index - valleys[valleys.length - 2].index); // 检查两条边界线是否都向下倾斜,且下部边界倾斜更缓 if (peakSlope < 0 && valleySlope < 0 && valleySlope > peakSlope) { // 计算下降楔形的上部边界 const upperBoundary1 = peaks[peaks.length - 2]; const upperBoundary2 = peaks[peaks.length - 1]; // 计算下降楔形的下部边界 const lowerBoundary1 = valleys[valleys.length - 2]; const lowerBoundary2 = valleys[valleys.length - 1]; // 确定形态开始和结束的索引 const startIndex = Math.min(upperBoundary1.index, lowerBoundary1.index); const endIndex = Math.max(upperBoundary2.index, lowerBoundary2.index); // 计算当前的预期边界位置 const currentIndex = data.length - 1; const projectedUpperBoundary = upperBoundary1.price + peakSlope * (currentIndex - upperBoundary1.index); const projectedLowerBoundary = lowerBoundary1.price + valleySlope * (currentIndex - lowerBoundary1.index); // 计算收敛点 let convergenceIndex = upperBoundary1.index; if (peakSlope !== valleySlope) { const interceptDiff = lowerBoundary1.price - upperBoundary1.price - (lowerBoundary1.index - upperBoundary1.index) * (valleySlope - peakSlope); convergenceIndex = upperBoundary1.index + interceptDiff / (valleySlope - peakSlope); } // 计算当前是否接近收敛点 const proximityToConvergence = 1 - Math.min(1, Math.abs(currentIndex - convergenceIndex) / 20); // 确定当前价格相对于楔形的位置 const currentPrice = data[data.length - 1].close; let status = PatternStatus.Forming; if (currentIndex > endIndex) { if (currentPrice > projectedUpperBoundary) { status = PatternStatus.Confirmed; // 已突破上方 } else if (currentPrice < projectedLowerBoundary) { status = PatternStatus.Failed; // 意外向下突破 } else { status = PatternStatus.Completed; // 形成但未突破 } } // 检查形态是否无效 - 下降楔形(看涨)形态 let isInvalid = false; // 检查1:如果之前确认了上方突破,后续K线又回到楔形内部或突破下边界,形态无效 if (status === PatternStatus.Confirmed && endIndex < data.length - 1) { let foundBreakout = false; let invalidAfterBreakout = false; for (let i = endIndex + 1; i < data.length; i++) { // 对于当前K线,计算预期的上下边界 const upperBound = upperBoundary1.price + peakSlope * (i - upperBoundary1.index); const lowerBound = lowerBoundary1.price + valleySlope * (i - lowerBoundary1.index); // 首先检测是否出现上方突破(形态确认) if (!foundBreakout && data[i].close > upperBound) { foundBreakout = true; continue; } // 如果已经确认上方突破,但后续K线回到楔形内部或突破下边界,形态无效 if ( foundBreakout && (data[i].close < upperBound || data[i].low < lowerBound) ) { invalidAfterBreakout = true; break; } } isInvalid = invalidAfterBreakout; } // 检查2:如果价格长时间在楔形内部盘整而没有突破,可能形态无效 if ( !isInvalid && currentIndex - endIndex > 15 && status === PatternStatus.Completed ) { isInvalid = true; // 形态完成后15个K线仍未突破,考虑无效 } // 如果形态无效,跳过此形态 if (isInvalid) { // 跳过此形态,不添加到结果中 // continue 在这里没有效果,因为我们不在循环内,所以直接使用 if-else 结构 } else { // 计算形态高度(在结束点的高度) const patternHeight = projectedUpperBoundary - projectedLowerBoundary; // 计算价格目标(向上突破的目标,通常是形态起始点的价格) const startHeight = upperBoundary1.price - lowerBoundary1.price; const priceTarget = upperBoundary1.price + startHeight; // 计算可靠性分数 const reliability = calculateWedgeReliability( data, startIndex, endIndex, patternHeight, proximityToConvergence, status === PatternStatus.Confirmed, PatternType.FallingWedge ); patterns.push({ patternType: PatternType.FallingWedge, status, direction: PatternDirection.Bullish, reliability, significance: reliability * (patternHeight / currentPrice), component: { startIndex, endIndex, keyPoints: [ upperBoundary1, upperBoundary2, lowerBoundary1, lowerBoundary2, ], patternHeight, breakoutLevel: projectedUpperBoundary, volumePattern: analyzeWedgeVolume( data, startIndex, endIndex, status ), }, priceTarget, stopLoss: projectedLowerBoundary * 0.98, // 下边界下方2% breakoutExpected: status === PatternStatus.Completed && proximityToConvergence > 0.7, breakoutDirection: PatternDirection.Bullish, probableBreakoutZone: [ projectedUpperBoundary * 0.98, projectedUpperBoundary * 1.02, ], description: `下降楔形, ${getStatusDescription(status)}, 上边界当前在 ${projectedUpperBoundary.toFixed(2)}, 下边界当前在 ${projectedLowerBoundary.toFixed(2)}`, tradingImplication: `看涨信号, 目标价位: ${priceTarget.toFixed(2)}, 止损位: ${(projectedLowerBoundary * 0.98).toFixed(2)}`, keyDates: [...peaks.map(p => p.date), ...valleys.map(v => v.date)], keyPrices: [...peaks.map(p => p.price), ...valleys.map(v => v.price)], }); } } } return patterns; } /** * 计算楔形形态的可靠性 */ function calculateWedgeReliability( data: Candle[], startIndex: number, endIndex: number, patternHeight: number, proximityToConvergence: number, isBreakoutConfirmed: boolean, patternType: PatternType ): number { let score = 50; // 初始可靠性分数 // 1. 形态持续时间(通常越长越好) const duration = endIndex - startIndex; if (duration > 20) score += 15; else if (duration > 10) score += 10; else score += 5; // 2. 形态收敛程度 score += proximityToConvergence * 15; // 3. 确认突破 if (isBreakoutConfirmed) score += 15; // 4. 形态与趋势的关系 if (patternType === PatternType.RisingWedge) { // 上升楔形在上升趋势末期更可靠 // 这里简化处理,实际应考虑趋势 score += 5; } else if (patternType === PatternType.FallingWedge) { // 下降楔形在下降趋势末期更可靠 score += 5; } // 5. 楔形角度(越窄越好) // 简化处理,假设已经是合理的楔形 score += 5; // 最后确保分数在0-100范围内 return Math.max(0, Math.min(100, score)); } /** * 分析楔形形态的成交量特征 */ function analyzeWedgeVolume( data: Candle[], startIndex: number, endIndex: number, status: PatternStatus ): string { const volumes = data.slice(startIndex, endIndex + 1).map(d => d.volume); const avgVolume = volumes.reduce((sum, v) => sum + v, 0) / volumes.length; // 检查突破时的成交量 let breakoutVolume = 0; if (status === PatternStatus.Confirmed && endIndex + 1 < data.length) { breakoutVolume = data[endIndex + 1].volume; } // 检查形态过程中的成交量趋势 const firstHalfAvg = volumes .slice(0, Math.ceil(volumes.length / 2)) .reduce((sum, v) => sum + v, 0) / Math.ceil(volumes.length / 2); const secondHalfAvg = volumes .slice(Math.ceil(volumes.length / 2)) .reduce((sum, v) => sum + v, 0) / (volumes.length - Math.ceil(volumes.length / 2)); const volumeTrendRatio = secondHalfAvg / firstHalfAvg; if (volumeTrendRatio < 0.8) { // 成交量减少是楔形形态的理想情况 if ( status === PatternStatus.Confirmed && breakoutVolume > avgVolume * 1.5 ) { return '理想的成交量模式:形态期间成交量逐渐减少,突破时成交量明显放大'; } else { return '良好的成交量模式:形态期间成交量逐渐减少'; } } else if (volumeTrendRatio > 1.2) { return '非典型的成交量模式:形态期间成交量增加而非减少,降低形态可靠性'; } else { return '成交量模式中性,无明显趋势'; } }