@gabriel3615/ta_analysis
Version:
stock ta analysis
506 lines (505 loc) • 19.5 kB
JavaScript
import { getStockData, percentChange, rollingMax, rollingMin, standardDeviation, } from '../util/util.js';
import { EMA, RSI, SMA } from 'technicalindicators';
import { calculateSlope } from '../util/taUtil.js';
export function extractPatternFeatures(candles) {
const analysis = analyzeStockPattern(candles);
// 保留原有代码...
let trendType;
if (Math.abs(analysis.trendSlope) < 0.01) {
trendType = 'sideways';
}
else {
trendType = analysis.trendSlope > 0 ? 'up' : 'down';
}
let volatilityLevel;
if (analysis.volatility > 0.4) {
volatilityLevel = 'high';
}
else if (analysis.volatility > 0.2) {
volatilityLevel = 'medium';
}
else {
volatilityLevel = 'low';
}
let volumePattern;
if (analysis.highVolumeDates.length >
Math.floor(analysis.highVolumeDates.length / 10)) {
volumePattern = 'spikes';
}
else if (Math.abs(analysis.avgVolumeRatio - 1) < 0.1) {
volumePattern = 'stable';
}
else {
volumePattern = analysis.avgVolumeRatio > 1 ? 'increasing' : 'decreasing';
}
let rsiState;
if (analysis.currentRsi > 70) {
rsiState = 'overbought';
}
else if (analysis.currentRsi < 30) {
rsiState = 'oversold';
}
else {
rsiState = 'neutral';
}
const supportResistanceCount = analysis.supportLevels.length + analysis.resistanceLevels.length;
let maConfiguration;
if (analysis.goldenCrossDates.length > 0 &&
analysis.deathCrossDates.length > 0) {
const lastGoldenCross = analysis.goldenCrossDates[analysis.goldenCrossDates.length - 1];
const lastDeathCross = analysis.deathCrossDates[analysis.deathCrossDates.length - 1];
if (lastGoldenCross > lastDeathCross) {
maConfiguration = 'above';
}
else {
maConfiguration = 'below';
}
}
else if (analysis.goldenCrossDates.length > 0) {
maConfiguration = 'above';
}
else if (analysis.deathCrossDates.length > 0) {
maConfiguration = 'below';
}
else {
maConfiguration = 'crossing';
}
// 计算波动率趋势
let volatilityTrend;
// 获取价格和日期数据
const prices = candles.map(candle => candle.close);
const highs = candles.map(candle => candle.high);
const lows = candles.map(candle => candle.low);
const opens = candles.map(candle => candle.open);
const volumes = candles.map(candle => candle.volume);
const windowSize = Math.min(20, Math.floor(prices.length / 4));
// 计算滚动波动率
const rollingVolatilities = [];
for (let i = windowSize; i < prices.length; i++) {
const windowPrices = prices.slice(i - windowSize, i);
const returns = percentChange(windowPrices);
rollingVolatilities.push(standardDeviation(returns) * Math.sqrt(252));
}
const volatilitySlope = calculateSlope(rollingVolatilities);
if (Math.abs(volatilitySlope) < 0.0005) {
volatilityTrend = 'stable';
}
else {
volatilityTrend = volatilitySlope < 0 ? 'decreasing' : 'increasing';
}
// 新逻辑:判断在波动率低的区域,价格位于交易区间的位置
let priceVolatilityPattern;
// 只在波动率低或波动率下降的情况下进行判断
if (volatilityLevel === 'low' || volatilityTrend === 'decreasing') {
// 获取最近一段时间的价格数据(例如最后10个交易日)
const recentPrices = prices.slice(-10);
const highestPrice = Math.max(...recentPrices);
const lowestPrice = Math.min(...recentPrices);
const priceRange = highestPrice - lowestPrice;
// 获取最近的收盘价
const lastPrice = prices[prices.length - 1];
// 计算最新价格在区间中的相对位置 (0 表示在最低点,1 表示在最高点)
const relativePosition = priceRange > 0 ? (lastPrice - lowestPrice) / priceRange : 0.5;
// 判断价格是在上部、中部还是下部
if (relativePosition > 0.67) {
priceVolatilityPattern = 'low_volatility_upper_range';
}
else if (relativePosition < 0.33) {
priceVolatilityPattern = 'low_volatility_lower_range';
}
else {
priceVolatilityPattern = 'low_volatility_mid_range';
}
}
else {
priceVolatilityPattern = 'other';
}
// 1. 价格行为模式识别
let priceAction;
// 检查最近的价格是否突破了前期阻力
if (prices.length >= 20) {
const recentPrice = prices[prices.length - 1];
const previousPrices = prices.slice(-20, -1);
const previousHigh = Math.max(...previousPrices);
// 计算前期20天的价格范围
const priceRange = Math.max(...previousPrices) - Math.min(...previousPrices);
const rangeMidpoint = Math.min(...previousPrices) + priceRange / 2;
const ema20Prices = EMA.calculate({
period: 20,
values: prices,
});
const ema20Price = ema20Prices[ema20Prices.length - 1];
const derivation = (recentPrice - ema20Price) / ema20Price;
const isNear = Math.abs(derivation) <= 0.03;
if (recentPrice > previousHigh * 1.02) {
priceAction = 'breakout';
}
else if (recentPrice < Math.min(...previousPrices) * 0.98) {
priceAction = 'breakdown';
}
else if (priceRange < previousHigh * 0.05) {
// 价格范围小于5%视为盘整
priceAction = 'consolidation';
}
else if (trendType === 'up' && isNear) {
priceAction = 'pullback';
}
else {
priceAction = 'other';
}
}
else {
priceAction = 'other';
}
// 2. 识别蜡烛图形态
const candlePatterns = [];
// 识别十字星形态
for (let i = 0; i < candles.length; i++) {
const body = Math.abs(candles[i].close - candles[i].open);
const totalRange = candles[i].high - candles[i].low;
if (body / totalRange < 0.1 && totalRange > 0) {
candlePatterns.push('doji');
break; // 只记录一次
}
}
// 识别锤子线
for (let i = 0; i < candles.length; i++) {
const bodyHigh = Math.max(candles[i].open, candles[i].close);
const bodyLow = Math.min(candles[i].open, candles[i].close);
const body = bodyHigh - bodyLow;
const upperShadow = candles[i].high - bodyHigh;
const lowerShadow = bodyLow - candles[i].low;
if (body > 0 && lowerShadow > body * 2 && upperShadow < body * 0.2) {
candlePatterns.push('hammer');
break; // 只记录一次
}
}
// 识别吞没形态
for (let i = 1; i < candles.length; i++) {
const current = candles[i];
const previous = candles[i - 1];
// 定义上涨和下跌的K线
const currentBullish = current.close > current.open;
const previousBullish = previous.close > previous.open;
// 看涨吞没形态
if (!previousBullish &&
currentBullish &&
current.close > previous.open &&
current.open < previous.close) {
candlePatterns.push('bullish_engulfing');
break; // 只记录一次
}
// 看跌吞没形态
if (previousBullish &&
!currentBullish &&
current.open > previous.close &&
current.close < previous.open) {
candlePatterns.push('bearish_engulfing');
break; // 只记录一次
}
}
// 3. 检查是否存在价格缺口
let gapPresent = false;
for (let i = 1; i < candles.length; i++) {
const current = candles[i];
const previous = candles[i - 1];
// 上涨缺口
if (current.low > previous.high) {
gapPresent = true;
break;
}
// 下跌缺口
if (current.high < previous.low) {
gapPresent = true;
break;
}
}
// 4. 分析价格与成交量的关系
let volumePriceRelationship;
if (prices.length >= 5 && volumes.length >= 5) {
const recentPrices = prices.slice(-5);
const recentVolumes = volumes.slice(-5);
const priceChange = recentPrices[recentPrices.length - 1] - recentPrices[0];
const volumeChange = recentVolumes[recentVolumes.length - 1] - recentVolumes[0];
if (priceChange > 0 && volumeChange > 0) {
volumePriceRelationship = 'rising_price_rising_volume';
}
else if (priceChange > 0 && volumeChange < 0) {
volumePriceRelationship = 'rising_price_falling_volume';
}
else if (priceChange < 0 && volumeChange > 0) {
volumePriceRelationship = 'falling_price_rising_volume';
}
else if (priceChange < 0 && volumeChange < 0) {
volumePriceRelationship = 'falling_price_falling_volume';
}
else {
volumePriceRelationship = 'neutral';
}
}
else {
volumePriceRelationship = 'neutral';
}
// 5. 分析波动高点和低点
let swingHighsLows;
if (prices.length >= 20) {
// 简单方法:将数据分成4段,比较第一段和最后一段的高点和低点
const segment1 = highs.slice(0, 5);
const segment4 = highs.slice(-5);
const segment1Lows = lows.slice(0, 5);
const segment4Lows = lows.slice(-5);
const segment1High = Math.max(...segment1);
const segment4High = Math.max(...segment4);
const segment1Low = Math.min(...segment1Lows);
const segment4Low = Math.min(...segment4Lows);
const higherHighs = segment4High > segment1High;
const higherLows = segment4Low > segment1Low;
if (higherHighs && higherLows) {
swingHighsLows = 'higher_highs_higher_lows';
}
else if (!higherHighs && !higherLows) {
swingHighsLows = 'lower_highs_lower_lows';
}
else if (higherHighs && !higherLows) {
swingHighsLows = 'higher_highs_lower_lows';
}
else if (!higherHighs && higherLows) {
swingHighsLows = 'lower_highs_higher_lows';
}
else {
swingHighsLows = 'flat';
}
}
else {
swingHighsLows = 'flat';
}
// 6. 识别价格通道类型
let priceChannelType;
if (prices.length >= 20) {
// 计算高点和低点的线性回归斜率
const highsSlope = calculateSlope(highs.slice(-20));
const lowsSlope = calculateSlope(lows.slice(-20));
// 计算通道宽度变化
const recentChannelWidth = Math.max(...highs.slice(-5)) - Math.min(...lows.slice(-5));
const earlierChannelWidth = Math.max(...highs.slice(-20, -15)) - Math.min(...lows.slice(-20, -15));
const channelWidthChange = recentChannelWidth - earlierChannelWidth;
if (Math.abs(highsSlope) < 0.0001 && Math.abs(lowsSlope) < 0.0001) {
priceChannelType = 'horizontal';
}
else if (highsSlope > 0.0001 && lowsSlope > 0.0001) {
priceChannelType = 'ascending';
}
else if (highsSlope < -0.0001 && lowsSlope < -0.0001) {
priceChannelType = 'descending';
}
else if (channelWidthChange > earlierChannelWidth * 0.2) {
priceChannelType = 'expanding';
}
else if (channelWidthChange < -earlierChannelWidth * 0.2) {
priceChannelType = 'contracting';
}
else {
priceChannelType = 'none';
}
}
else {
priceChannelType = 'none';
}
return {
// 现有特征...
trendType,
volatilityLevel,
volumePattern,
rsiState,
supportResistanceCount,
maConfiguration,
volatilityTrend,
priceVolatilityPattern,
// 新增特征
priceAction,
candlePatterns,
gapPresent,
volumePriceRelationship,
swingHighsLows,
priceChannelType,
};
}
// 3. 权重可能需要调整,特别是新的 priceVolatilityPattern 特征
export function matchStockPattern(symbol, sourcePatterns, targetPatterns) {
// 匹配每个特征
const featureMatches = {
trendType: targetPatterns.trendType === sourcePatterns.trendType,
volatilityLevel: targetPatterns.volatilityLevel === sourcePatterns.volatilityLevel,
volumePattern: targetPatterns.volumePattern === sourcePatterns.volumePattern,
rsiState: targetPatterns.rsiState === sourcePatterns.rsiState,
maConfiguration: targetPatterns.maConfiguration === sourcePatterns.maConfiguration,
supportResistanceCount: Math.abs(targetPatterns.supportResistanceCount -
sourcePatterns.supportResistanceCount) <= 2,
volatilityTrend: targetPatterns.volatilityTrend === sourcePatterns.volatilityTrend,
priceVolatilityPattern: targetPatterns.priceVolatilityPattern ===
sourcePatterns.priceVolatilityPattern,
priceAction: targetPatterns.priceAction === sourcePatterns.priceAction,
// 对于蜡烛图形态,检查是否有至少一个共同模式
candlePatterns: sourcePatterns.candlePatterns.some(pattern => targetPatterns.candlePatterns.includes(pattern)) ||
(sourcePatterns.candlePatterns.length === 0 &&
targetPatterns.candlePatterns.length === 0),
gapPresent: targetPatterns.gapPresent === sourcePatterns.gapPresent,
volumePriceRelationship: targetPatterns.volumePriceRelationship ===
sourcePatterns.volumePriceRelationship,
swingHighsLows: targetPatterns.swingHighsLows === sourcePatterns.swingHighsLows,
priceChannelType: targetPatterns.priceChannelType === sourcePatterns.priceChannelType,
};
// 计算匹配得分 (0-100)
const weights = {
trendType: 0.08,
volatilityLevel: 0.06,
volumePattern: 0.06,
rsiState: 0.06,
maConfiguration: 0.08,
supportResistanceCount: 0.03,
volatilityTrend: 0.07,
priceVolatilityPattern: 0.1,
priceAction: 0.1,
candlePatterns: 0.08,
gapPresent: 0.05,
volumePriceRelationship: 0.08,
swingHighsLows: 0.07,
priceChannelType: 0.08,
};
let score = 0;
for (const [feature, isMatch] of Object.entries(featureMatches)) {
if (isMatch) {
score += weights[feature] * 100;
}
}
// 确定是否匹配 (分数 >= 75 被认为是匹配)
const isMatch = score >= 75;
return {
symbol,
isMatch,
matchScore: `${score}/100`,
featureMatches,
features: targetPatterns,
};
}
// Main function: analyze stock pattern
function analyzeStockPattern(candles) {
const dates = candles.map(candle => candle.timestamp);
const prices = candles.map(candle => candle.close);
const volumes = candles.map(candle => candle.volume);
// 1. Trend analysis
const slope = calculateSlope(prices);
const trendDirection = slope > 0 ? 'up' : 'down';
// 2. Support and resistance levels identification
// computes the window size as one-twentieth of the length of the prices array,
// ensures that the window size is at least 10, even if the calculated value is smaller
const window = Math.max(10, Math.floor(prices.length / 20)); // Dynamic window size
const rollingMins = rollingMin(prices, window);
const rollingMaxs = rollingMax(prices, window);
const supportLevels = [];
const resistanceLevels = [];
for (let i = window; i < prices.length - window; i++) {
if (prices[i] === rollingMins[i] && !isNaN(rollingMins[i])) {
const isNearExisting = supportLevels.some(level => Math.abs(level - prices[i]) / prices[i] < 0.02);
if (!isNearExisting) {
supportLevels.push(prices[i]);
}
}
if (prices[i] === rollingMaxs[i] && !isNaN(rollingMaxs[i])) {
const isNearExisting = resistanceLevels.some(level => Math.abs(level - prices[i]) / prices[i] < 0.02);
if (!isNearExisting) {
resistanceLevels.push(prices[i]);
}
}
}
// 3. Moving average analysis - using EMA instead of SMA
const shortMa = EMA.calculate({
period: 10,
values: prices,
});
const longMa = EMA.calculate({
period: 20,
values: prices,
});
const goldenCrossDates = [];
const deathCrossDates = [];
for (let i = 1; i < prices.length; i++) {
if (!isNaN(shortMa[i]) &&
!isNaN(longMa[i]) &&
!isNaN(shortMa[i - 1]) &&
!isNaN(longMa[i - 1])) {
if (shortMa[i] > longMa[i] && shortMa[i - 1] <= longMa[i - 1]) {
goldenCrossDates.push(dates[i]);
}
if (shortMa[i] < longMa[i] && shortMa[i - 1] >= longMa[i - 1]) {
deathCrossDates.push(dates[i]);
}
}
}
// 4. Volume analysis
const avgVolume = SMA.calculate({ period: 20, values: volumes });
const volumeRatio = [];
for (let i = 0; i < volumes.length; i++) {
volumeRatio.push(isNaN(avgVolume[i]) ? NaN : volumes[i] / avgVolume[i]);
}
const highVolumeDates = [];
let validVolumeRatios = 0;
let sumVolumeRatio = 0;
for (let i = 0; i < volumeRatio.length; i++) {
if (!isNaN(volumeRatio[i])) {
if (volumeRatio[i] > 2) {
highVolumeDates.push(dates[i]);
}
sumVolumeRatio += volumeRatio[i];
validVolumeRatios++;
}
}
const avgVolumeRatio = validVolumeRatios > 0 ? sumVolumeRatio / validVolumeRatios : 0;
// 5. Volatility analysis
const dailyReturns = percentChange(prices);
const annualizedVolatility = standardDeviation(dailyReturns) * Math.sqrt(252);
// 6. RSI analysis
const rsi = RSI.calculate({
period: 14,
values: prices,
});
const currentRsi = rsi[rsi.length - 1] || 0;
let overboughtDays = 0;
let oversoldDays = 0;
for (const rsiValue of rsi) {
if (!isNaN(rsiValue)) {
if (rsiValue > 70)
overboughtDays++;
if (rsiValue < 30)
oversoldDays++;
}
}
return {
trendSlope: slope,
trendDirection,
supportLevels,
resistanceLevels,
goldenCrossDates,
deathCrossDates,
highVolumeDates,
avgVolumeRatio,
volatility: annualizedVolatility,
currentRsi,
overboughtDays,
oversoldDays,
};
}
const main = async () => {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - 30);
const sourceCandles = await getStockData('COIN', startDate, endDate);
const sourceFeatures = extractPatternFeatures(sourceCandles);
console.log('Source features:', sourceFeatures);
const targetCandles = await getStockData('MSTR', startDate, endDate);
const targetFeatures = extractPatternFeatures(targetCandles);
console.log('Target features:', targetFeatures);
const result = matchStockPattern('MSTR', sourceFeatures, targetFeatures);
console.log('Match result:', result);
};
// main();