649 lines
22 KiB
TypeScript
649 lines
22 KiB
TypeScript
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;
|
|
}
|