diff --git a/src/helpers/candles.ts b/src/helpers/candles.ts new file mode 100644 index 0000000..f6b1dbd --- /dev/null +++ b/src/helpers/candles.ts @@ -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; +} diff --git a/src/services/indicatorService.ts b/src/services/indicatorService.ts index 91a709d..4e3b926 100644 --- a/src/services/indicatorService.ts +++ b/src/services/indicatorService.ts @@ -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] }; } diff --git a/test.ts b/test.ts index 8cae5e2..7524268 100644 --- a/test.ts +++ b/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); -})(); \ No newline at end of file + const cA = analyzeCandleSequence([candles[2], candles[1], candles[0]]); + const isH = isHighVolatilityCandle(candles.reverse()); + console.log(cA); + console.log("isHighVolatilityCandle", isH); +})();