update
This commit is contained in:
parent
dbf3b758fe
commit
e45c62215e
3 changed files with 723 additions and 35 deletions
649
src/helpers/candles.ts
Normal file
649
src/helpers/candles.ts
Normal file
|
|
@ -0,0 +1,649 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,27 +1,54 @@
|
|||
import { EMA, MACD } from 'technicalindicators';
|
||||
import { Candle } from '../dao/candles';
|
||||
import { EMA, MACD } from "technicalindicators";
|
||||
import { Candle } from "../dao/candles";
|
||||
import { analyzeCandleSequence } from "../helpers/candles";
|
||||
|
||||
export function analyze(candles: Candle[]): { ema34: number; ema200: number; macd: any } {
|
||||
let close = candles.map(c => c.close);
|
||||
const ema34 = EMA.calculate({ period: 34, values: close, reversedInput: true });
|
||||
const ema200 = EMA.calculate({ period: 200, values: close, reversedInput: true });
|
||||
const macd = MACD.calculate({
|
||||
values: close,
|
||||
fastPeriod: 45,
|
||||
slowPeriod: 90,
|
||||
signalPeriod: 9,
|
||||
SimpleMAOscillator: false,
|
||||
SimpleMASignal: false,
|
||||
reversedInput: true
|
||||
});
|
||||
const candle = candles[0];
|
||||
if (candle.low < ema200[0] && candle.high > ema200[0] && candle.close > ema200[0]) {
|
||||
console.log('Buy');
|
||||
export class IndicatorService {
|
||||
constructor() {}
|
||||
|
||||
analyze(candles: Candle[]): { ema34: number; ema200: number; macd: any } {
|
||||
let close = candles.map((c) => c.close);
|
||||
const ema34 = EMA.calculate({
|
||||
period: 34,
|
||||
values: close,
|
||||
reversedInput: true,
|
||||
});
|
||||
const ema200 = EMA.calculate({
|
||||
period: 200,
|
||||
values: close,
|
||||
reversedInput: true,
|
||||
});
|
||||
const macd = MACD.calculate({
|
||||
values: close,
|
||||
fastPeriod: 45,
|
||||
slowPeriod: 90,
|
||||
signalPeriod: 9,
|
||||
SimpleMAOscillator: false,
|
||||
SimpleMASignal: false,
|
||||
reversedInput: true,
|
||||
});
|
||||
const candle = candles[0];
|
||||
if (
|
||||
candle.low < ema200[0] &&
|
||||
candle.high > ema200[0] &&
|
||||
candle.close > ema200[0]
|
||||
) {
|
||||
console.log("Buy");
|
||||
}
|
||||
|
||||
if (
|
||||
candle.high > ema200[0] &&
|
||||
candle.low < ema200[0] &&
|
||||
candle.close < ema200[0]
|
||||
) {
|
||||
console.log("Sell");
|
||||
}
|
||||
|
||||
const candleAnalysis = analyzeCandleSequence([
|
||||
candles[2],
|
||||
candles[1],
|
||||
candles[0],
|
||||
]);
|
||||
|
||||
return { ema34: ema34[0], ema200: ema200[0], macd: macd[0] };
|
||||
}
|
||||
|
||||
if (candle.high > ema200[0] && candle.low < ema200[0] && candle.close < ema200[0]) {
|
||||
console.log('Sell');
|
||||
}
|
||||
|
||||
return { ema34: ema34[0], ema200: ema200[0], macd: macd[0] };
|
||||
}
|
||||
|
|
|
|||
32
test.ts
32
test.ts
|
|
@ -1,14 +1,26 @@
|
|||
import 'dotenv/config';
|
||||
import { BybitService } from './src/services/bybitService';
|
||||
import * as indicatorService from './src/services/indicatorService';
|
||||
import { sendLarkMessage } from './src/services/messageService';
|
||||
import "dotenv/config";
|
||||
import { BybitService } from "./src/services/bybitService";
|
||||
import * as indicatorService from "./src/services/indicatorService";
|
||||
import {
|
||||
analyzeCandleSequence,
|
||||
isHighVolatilityCandle,
|
||||
} from "./src/helpers/candles";
|
||||
|
||||
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!, false);
|
||||
const bybitService = new BybitService(
|
||||
process.env.BYBIT_API_KEY!,
|
||||
process.env.BYBIT_API_SECRET!,
|
||||
false
|
||||
);
|
||||
(async () => {
|
||||
const candles = await bybitService.getCandles({ symbol: 'BTCUSDT', interval: '15', category: 'linear', limit: 500 });
|
||||
const candles = await bybitService.getCandles({
|
||||
symbol: "BTCUSDT",
|
||||
interval: "240",
|
||||
category: "linear",
|
||||
limit: 500,
|
||||
});
|
||||
console.log(candles[0], candles[1]);
|
||||
const analysis = indicatorService.analyze(candles);
|
||||
console.log(analysis);
|
||||
const message = JSON.stringify(analysis);
|
||||
await sendLarkMessage('oc_f9b2e8f0309ecab0c94e3e134b0ddd29', message);
|
||||
const cA = analyzeCandleSequence([candles[2], candles[1], candles[0]]);
|
||||
const isH = isHighVolatilityCandle(candles.reverse());
|
||||
console.log(cA);
|
||||
console.log("isHighVolatilityCandle", isH);
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue