interface Candle { open: number; close: number; low: number; high: number; } type PatternType = | "Doji" | "Hammer" | "Dragon Doji" | "Shooting Star" | "Inverse Hammer" | "Hanging Man" // Thêm Hanging Man | "Bullish Engulfing" | "Bearish Engulfing" | "Piercing Pattern" | "Dark Cloud Cover" | "Morning Star" | "Evening Star" | "Three White Soldiers" | "Three Black Crows" | "None"; type ReversalDirection = | "Bullish Reversal" | "Bearish Reversal" | "Potential Reversal" | "None"; interface CandleAnalysis { pattern: PatternType; reversalDirection: ReversalDirection; isReversal: boolean; } // Global parameters for pattern detection const BODY_TOLERANCE_PERCENT = 0.05; // For Doji: body size relative to total range const WICK_FACTOR = 2; // For Hammer/Shooting Star: wick size relative to body size const SMALL_BODY_MAX_PERCENT = 0.3; // Max body size for patterns like Hammer/Shooting Star (30% of total range) const BODY_POSITION_THRESHOLD = 0.7; // For Hammer: body in upper 30% const INVERSE_BODY_POSITION_THRESHOLD = 0.3; // For Inverse Hammer: body in lower 30% const WICK_MIN_PERCENT_OF_RANGE = 0.6; // For Dragon Doji: lower wick is at least 60% of total range const PIERCING_DARKCLOUD_THRESHOLD = 0.5; // For Piercing/Dark Cloud: close must be above/below midpoint of previous body // --- Helper Functions --- function isBullish(candle: Candle): boolean { return candle.close > candle.open; } function isBearish(candle: Candle): boolean { return candle.close < candle.open; } function getBodySize(candle: Candle): number { return Math.abs(candle.open - candle.close); } function getTotalRange(candle: Candle): number { return candle.high - candle.low; } function getUpperWick(candle: Candle): number { return candle.high - Math.max(candle.open, candle.close); } function getLowerWick(candle: Candle): number { return Math.min(candle.open, candle.close) - candle.low; } // --- Single-Candle Patterns --- /** * Doji: open and close are virtually the same. */ function isDoji(candle: Candle): boolean { const range = getTotalRange(candle); if (range === 0) return true; const bodySize = getBodySize(candle); return bodySize <= range * BODY_TOLERANCE_PERCENT; } /** * Hammer: Small body at the top, long lower wick, little/no upper wick. Bullish reversal. */ function isHammer(candle: Candle): boolean { const bodySize = getBodySize(candle); const totalRange = getTotalRange(candle); if (totalRange === 0) return false; const lowerWick = getLowerWick(candle); const upperWick = getUpperWick(candle); // Small body const hasSmallBody = bodySize <= totalRange * SMALL_BODY_MAX_PERCENT; // Long lower wick (at least WICK_FACTOR times body size) const hasLongLowerWick = lowerWick >= WICK_FACTOR * bodySize && lowerWick > 0; // Little/no upper wick const hasSmallUpperWick = upperWick < totalRange * 0.1 || upperWick < bodySize * 0.5; // Body in the upper part (at least 70% from the low) const isBodyInUpperPart = (Math.max(candle.open, candle.close) - candle.low) / totalRange >= BODY_POSITION_THRESHOLD; return ( hasSmallBody && hasLongLowerWick && hasSmallUpperWick && isBodyInUpperPart ); } /** * Inverse Hammer: Small body at the bottom, long upper wick, little/no lower wick. Bullish reversal. */ function isInverseHammer(candle: Candle): boolean { const bodySize = getBodySize(candle); const totalRange = getTotalRange(candle); if (totalRange === 0) return false; const lowerWick = getLowerWick(candle); const upperWick = getUpperWick(candle); // Small body const hasSmallBody = bodySize <= totalRange * SMALL_BODY_MAX_PERCENT; // Long upper wick (at least WICK_FACTOR times body size) const hasLongUpperWick = upperWick >= WICK_FACTOR * bodySize && upperWick > 0; // Little/no lower wick const hasSmallLowerWick = lowerWick < totalRange * 0.1 || lowerWick < bodySize * 0.5; // Body in the lower part (at most 30% from the low) const isBodyInLowerPart = (Math.max(candle.open, candle.close) - candle.low) / totalRange <= INVERSE_BODY_POSITION_THRESHOLD; return ( hasSmallBody && hasLongUpperWick && hasSmallLowerWick && isBodyInLowerPart ); } /** * Dragon Doji: Open, close, high are virtually the same, long lower wick. Bullish reversal. */ function isDragonDoji(candle: Candle): boolean { const range = getTotalRange(candle); if (range === 0) return false; // Open, close, high are virtually the same (Doji-like body at the high) const upperBodySimilarity = Math.abs(candle.open - candle.high) <= range * BODY_TOLERANCE_PERCENT && Math.abs(candle.close - candle.high) <= range * BODY_TOLERANCE_PERCENT && Math.abs(candle.open - candle.close) <= range * BODY_TOLERANCE_PERCENT; if (!upperBodySimilarity) return false; const lowerWick = getLowerWick(candle); // Long lower wick (significant portion of the candle's total range) const hasLongLowerWick = lowerWick > range * WICK_MIN_PERCENT_OF_RANGE; // e.g., > 60% return hasLongLowerWick && lowerWick > 0.001; // Ensure a distinct lower wick exists } /** * Shooting Star: Small body at the bottom, long upper wick, little/no lower wick. Bearish reversal. */ function isShootingStar(candle: Candle): boolean { const bodySize = getBodySize(candle); const totalRange = getTotalRange(candle); if (totalRange === 0) return false; const lowerWick = getLowerWick(candle); const upperWick = getUpperWick(candle); // Small body const hasSmallBody = bodySize <= totalRange * SMALL_BODY_MAX_PERCENT; // Long upper wick (at least WICK_FACTOR times body size) const hasLongUpperWick = upperWick >= WICK_FACTOR * bodySize && upperWick > 0; // Little/no lower wick const hasSmallLowerWick = lowerWick < totalRange * 0.1 || lowerWick < bodySize * 0.5; // Body in the lower part (at most 30% from the high, or 70% from the low) const isBodyInLowerPart = (Math.max(candle.open, candle.close) - candle.low) / totalRange <= INVERSE_BODY_POSITION_THRESHOLD; return ( hasSmallBody && hasLongUpperWick && hasSmallLowerWick && isBodyInLowerPart ); } /** * Hanging Man: Small body at the top, long lower wick, little/no upper wick. Bearish reversal. * Similar to Hammer, but appears in an uptrend. The pattern itself is the same, context defines its name/implication. */ function isHangingMan(candle: Candle): boolean { // Hanging Man has the exact same visual characteristics as a Hammer. // The distinction is the preceding trend (uptrend for Hanging Man, downtrend for Hammer). // For this function, we only check the candle's shape. // The `analyzeCandleSequence` will infer the context if you pass previous candles. return isHammer(candle); } // --- Two-Candle Patterns --- /** * Bullish Engulfing: A small bearish candle followed by a large bullish candle that completely engulfs the previous body. */ function isBullishEngulfing(candles: Candle[]): boolean { if (candles.length < 2) return false; const prevCandle = candles[candles.length - 2]; const currentCandle = candles[candles.length - 1]; // Previous candle must be bearish, current must be bullish if (!isBearish(prevCandle) || !isBullish(currentCandle)) return false; // Current candle body must engulf previous candle body const prevBodyMin = Math.min(prevCandle.open, prevCandle.close); const prevBodyMax = Math.max(prevCandle.open, prevCandle.close); const currBodyMin = Math.min(currentCandle.open, currentCandle.close); const currBodyMax = Math.max(currentCandle.open, currentCandle.close); return currBodyMin <= prevBodyMin && currBodyMax >= prevBodyMax; } /** * Bearish Engulfing: A small bullish candle followed by a large bearish candle that completely engulfs the previous body. */ function isBearishEngulfing(candles: Candle[]): boolean { if (candles.length < 2) return false; const prevCandle = candles[candles.length - 2]; const currentCandle = candles[candles.length - 1]; // Previous candle must be bullish, current must be bearish if (!isBullish(prevCandle) || !isBearish(currentCandle)) return false; // Current candle body must engulf previous candle body const prevBodyMin = Math.min(prevCandle.open, prevCandle.close); const prevBodyMax = Math.max(prevCandle.open, prevCandle.close); const currBodyMin = Math.min(currentCandle.open, currentCandle.close); const currBodyMax = Math.max(currentCandle.open, currentCandle.close); return currBodyMin <= prevBodyMin && currBodyMax >= prevBodyMax; } /** * Piercing Pattern: A long bearish candle followed by a bullish candle that opens below the low of the previous and closes above its midpoint. */ function isPiercingPattern(candles: Candle[]): boolean { if (candles.length < 2) return false; const prevCandle = candles[candles.length - 2]; const currentCandle = candles[candles.length - 1]; // Previous candle must be long and bearish if ( !isBearish(prevCandle) || getBodySize(prevCandle) < getTotalRange(prevCandle) * 0.6 ) return false; // Long body // Current candle must be bullish if (!isBullish(currentCandle)) return false; // Current candle opens below previous candle's low if (currentCandle.open >= prevCandle.low) return false; // Current candle closes above the midpoint of the previous candle's body const prevMidpoint = prevCandle.close + getBodySize(prevCandle) * PIERCING_DARKCLOUD_THRESHOLD; return currentCandle.close > prevMidpoint; } /** * Dark Cloud Cover: A long bullish candle followed by a bearish candle that opens above the high of the previous and closes below its midpoint. */ function isDarkCloudCover(candles: Candle[]): boolean { if (candles.length < 2) return false; const prevCandle = candles[candles.length - 2]; const currentCandle = candles[candles.length - 1]; // Previous candle must be long and bullish if ( !isBullish(prevCandle) || getBodySize(prevCandle) < getTotalRange(prevCandle) * 0.6 ) return false; // Long body // Current candle must be bearish if (!isBearish(currentCandle)) return false; // Current candle opens above previous candle's high if (currentCandle.open <= prevCandle.high) return false; // Current candle closes below the midpoint of the previous candle's body const prevMidpoint = prevCandle.open - getBodySize(prevCandle) * PIERCING_DARKCLOUD_THRESHOLD; return currentCandle.close < prevMidpoint; } // --- Three-Candle Patterns --- /** * Morning Star: Long bearish, small body (gap down), long bullish. Bullish reversal. */ function isMorningStar(candles: Candle[]): boolean { if (candles.length < 3) return false; const candle1 = candles[candles.length - 3]; const candle2 = candles[candles.length - 2]; const candle3 = candles[candles.length - 1]; // Candle 1: Long bearish candle if ( !isBearish(candle1) || getBodySize(candle1) < getTotalRange(candle1) * 0.6 ) return false; // Candle 2: Small body (could be Doji or Spinning Top) and gaps down if (getBodySize(candle2) > getTotalRange(candle2) * 0.3) return false; // Small body if (candle2.high >= candle1.close) return false; // Gap down (high of candle2 must be below close of candle1) // Candle 3: Long bullish candle, closes above midpoint of candle 1 if ( !isBullish(candle3) || getBodySize(candle3) < getTotalRange(candle3) * 0.6 ) return false; const candle1Midpoint = candle1.close + getBodySize(candle1) * PIERCING_DARKCLOUD_THRESHOLD; if (candle3.close <= candle1Midpoint) return false; return true; } /** * Evening Star: Long bullish, small body (gap up), long bearish. Bearish reversal. */ function isEveningStar(candles: Candle[]): boolean { if (candles.length < 3) return false; const candle1 = candles[candles.length - 3]; const candle2 = candles[candles.length - 2]; const candle3 = candles[candles.length - 1]; // Candle 1: Long bullish candle if ( !isBullish(candle1) || getBodySize(candle1) < getTotalRange(candle1) * 0.6 ) return false; // Candle 2: Small body (could be Doji or Spinning Top) and gaps up if (getBodySize(candle2) > getTotalRange(candle2) * 0.3) return false; // Small body if (candle2.low <= candle1.close) return false; // Gap up (low of candle2 must be above close of candle1) // Candle 3: Long bearish candle, closes below midpoint of candle 1 if ( !isBearish(candle3) || getBodySize(candle3) < getTotalRange(candle3) * 0.6 ) return false; const candle1Midpoint = candle1.open - getBodySize(candle1) * PIERCING_DARKCLOUD_THRESHOLD; if (candle3.close >= candle1Midpoint) return false; return true; } /** * Three White Soldiers: Three consecutive long bullish candles. Bullish reversal. */ function isThreeWhiteSoldiers(candles: Candle[]): boolean { if (candles.length < 3) return false; const c1 = candles[candles.length - 3]; const c2 = candles[candles.length - 2]; const c3 = candles[candles.length - 1]; // All three must be bullish candles with relatively long bodies if (!isBullish(c1) || !isBullish(c2) || !isBullish(c3)) return false; if ( getBodySize(c1) < getTotalRange(c1) * 0.4 || getBodySize(c2) < getTotalRange(c2) * 0.4 || getBodySize(c3) < getTotalRange(c3) * 0.4 ) return false; // At least 40% of range // Each opens within previous body and closes higher than previous close if (c2.open <= c1.open || c2.open >= c1.close) return false; // Open within body of previous if (c2.close <= c1.close) return false; // Close higher than previous if (c3.open <= c2.open || c3.open >= c2.close) return false; // Open within body of previous if (c3.close <= c2.close) return false; // Close higher than previous // Little to no upper wicks if ( getUpperWick(c1) > getBodySize(c1) * 0.5 || getUpperWick(c2) > getBodySize(c2) * 0.5 || getUpperWick(c3) > getBodySize(c3) * 0.5 ) return false; return true; } /** * Three Black Crows: Three consecutive long bearish candles. Bearish reversal. */ function isThreeBlackCrows(candles: Candle[]): boolean { if (candles.length < 3) return false; const c1 = candles[candles.length - 3]; const c2 = candles[candles.length - 2]; const c3 = candles[candles.length - 1]; // All three must be bearish candles with relatively long bodies if (!isBearish(c1) || !isBearish(c2) || !isBearish(c3)) return false; if ( getBodySize(c1) < getTotalRange(c1) * 0.4 || getBodySize(c2) < getTotalRange(c2) * 0.4 || getBodySize(c3) < getTotalRange(c3) * 0.4 ) return false; // At least 40% of range // Each opens within previous body and closes lower than previous close if (c2.open >= c1.open || c2.open <= c1.close) return false; // Open within body of previous if (c2.close >= c1.close) return false; // Close lower than previous if (c3.open >= c2.open || c3.open <= c2.close) return false; // Open within body of previous if (c3.close >= c2.close) return false; // Close lower than previous // Little to no lower wicks if ( getLowerWick(c1) > getBodySize(c1) * 0.5 || getLowerWick(c2) > getBodySize(c2) * 0.5 || getLowerWick(c3) > getBodySize(c3) * 0.5 ) return false; return true; } // --- Main Analysis Function --- /** * Analyzes a sequence of candles to determine its pattern and reversal characteristics. * IMPORTANT: Pass at least 3 candles for multi-candle patterns to be detected correctly. * The most recent candle should be at the end of the array. * @param candles An array of Candle objects. * @returns An object containing the pattern type, reversal direction, and whether it's a reversal. */ export function analyzeCandleSequence(candles: Candle[]): CandleAnalysis { if (candles.length === 0) { return { pattern: "None", reversalDirection: "None", isReversal: false }; } const currentCandle = candles[candles.length - 1]; // The latest candle // Multi-candle patterns (check with more historical data first) if (candles.length >= 3) { if (isMorningStar(candles)) { return { pattern: "Morning Star", reversalDirection: "Bullish Reversal", isReversal: true, }; } if (isEveningStar(candles)) { return { pattern: "Evening Star", reversalDirection: "Bearish Reversal", isReversal: true, }; } if (isThreeWhiteSoldiers(candles)) { return { pattern: "Three White Soldiers", reversalDirection: "Bullish Reversal", isReversal: true, }; } if (isThreeBlackCrows(candles)) { return { pattern: "Three Black Crows", reversalDirection: "Bearish Reversal", isReversal: true, }; } } if (candles.length >= 2) { if (isBullishEngulfing(candles)) { return { pattern: "Bullish Engulfing", reversalDirection: "Bullish Reversal", isReversal: true, }; } if (isBearishEngulfing(candles)) { return { pattern: "Bearish Engulfing", reversalDirection: "Bearish Reversal", isReversal: true, }; } if (isPiercingPattern(candles)) { return { pattern: "Piercing Pattern", reversalDirection: "Bullish Reversal", isReversal: true, }; } if (isDarkCloudCover(candles)) { return { pattern: "Dark Cloud Cover", reversalDirection: "Bearish Reversal", isReversal: true, }; } } // Single-candle patterns // Order matters: More specific patterns should be checked before more general ones. // For example, Dragon Doji is a type of Doji, so check it first. // Hammer and Hanging Man have identical shapes, distinguish by context if needed elsewhere. if (isHammer(currentCandle)) { // If you had trend detection, you could differentiate Hammer (downtrend) vs Hanging Man (uptrend) here. // For now, assuming shape check: return { pattern: "Hammer", reversalDirection: "Bullish Reversal", isReversal: true, }; // Default to Hammer if no trend context } if (isShootingStar(currentCandle)) { return { pattern: "Shooting Star", reversalDirection: "Bearish Reversal", isReversal: true, }; } if (isDragonDoji(currentCandle)) { return { pattern: "Dragon Doji", reversalDirection: "Bullish Reversal", isReversal: true, }; } if (isInverseHammer(currentCandle)) { return { pattern: "Inverse Hammer", reversalDirection: "Bullish Reversal", isReversal: true, }; } // Doji is checked last among single-candle patterns because other patterns might be more specific forms of Doji (like Dragon Doji) if (isDoji(currentCandle)) { return { pattern: "Doji", reversalDirection: "Potential Reversal", isReversal: true, }; } return { pattern: "None", reversalDirection: "None", isReversal: false }; } // Global parameters (có thể điều chỉnh) const HIGH_VOLATILITY_RANGE_MULTIPLIER = 1.5; // Phạm vi nến phải lớn hơn 1.5 lần so với ATR hoặc trung bình const HIGH_VOLATILITY_BODY_MULTIPLIER = 1.5; // Thân nến phải lớn hơn 1.5 lần so với trung bình thân nến const MIN_CANDLES_FOR_AVERAGE = 10; // Số lượng nến tối thiểu để tính trung bình /** * Calculates the Average True Range (ATR) for a given set of candles. * ATR is a measure of market volatility. * @param candles An array of Candle objects. * @returns The ATR value. */ export function calculateATR(candles: Candle[]): number { if (candles.length < 2) return 0; // Need at least 2 candles to calculate first TR let trueRanges: number[] = []; for (let i = 1; i < candles.length; i++) { const prevClose = candles[i - 1].close; const currentCandle = candles[i]; const tr1 = currentCandle.high - currentCandle.low; const tr2 = Math.abs(currentCandle.high - prevClose); const tr3 = Math.abs(currentCandle.low - prevClose); trueRanges.push(Math.max(tr1, tr2, tr3)); } // Simple Moving Average of True Range (can be replaced with Exponential Moving Average for more responsiveness) const sumOfTrueRanges = trueRanges.reduce((sum, tr) => sum + tr, 0); return sumOfTrueRanges / trueRanges.length; } /** * Calculates the average body size for a given set of candles. * @param candles An array of Candle objects. * @returns The average body size. */ export function calculateAverageBodySize(candles: Candle[]): number { if (candles.length === 0) return 0; const totalBodySize = candles.reduce( (sum, candle) => sum + getBodySize(candle), 0 ); return totalBodySize / candles.length; } /** * Checks if the most recent candle in a sequence exhibits high volatility. * This function considers both range and body size relative to historical data. * @param candles An array of Candle objects, with the most recent candle at the end. * @returns True if the latest candle shows high volatility, false otherwise. */ export function isHighVolatilityCandle(candles: Candle[]): boolean { if (candles.length === 0) return false; const currentCandle = candles[candles.length - 1]; // If not enough historical candles, we cannot compare against average if (candles.length < MIN_CANDLES_FOR_AVERAGE) { // For very few candles, we can define "high volatility" as simply a very large absolute range/body // You might want to define absolute thresholds here for initial candles. // E.g., return getTotalRange(currentCandle) > 5 || getBodySize(currentCandle) > 3; // For now, return false if not enough history for comparative analysis. return false; } // Take a look at historical data (e.g., the last MIN_CANDLES_FOR_AVERAGE candles) const historicalCandles = candles.slice(-MIN_CANDLES_FOR_AVERAGE - 1, -1); // Exclude the current candle itself // Calculate average true range (ATR) for volatility comparison const averageTrueRange = calculateATR(historicalCandles); // Calculate average body size for comparison const averageBodySize = calculateAverageBodySize(historicalCandles); const currentCandleRange = getTotalRange(currentCandle); const currentCandleBodySize = getBodySize(currentCandle); // Criteria for high volatility: // 1. Current candle's total range is significantly larger than historical average range (e.g., ATR) const isRangeHigh = currentCandleRange > averageTrueRange * HIGH_VOLATILITY_RANGE_MULTIPLIER; // 2. Current candle's body size is significantly larger than historical average body size const isBodySizeHigh = currentCandleBodySize > averageBodySize * HIGH_VOLATILITY_BODY_MULTIPLIER; // A candle is considered high volatility if either its range or its body size (or both) are significantly larger return isRangeHigh || isBodySizeHigh; }