import { BollingerBands, EMA, MACD } from "technicalindicators"; import { Candle } from "../dao/candles"; import { analyzeCandleSequence, isHighVolatilityCandle, isPinBar, } from "../helpers/candles"; import { Analysis } from "../dao/analysis"; import { KlineIntervalV3 } from "bybit-api"; import { Order } from "../dao/order"; import { supabase } from "./supabaseService"; import { Wave } from "../dao/wave"; export type EventType = | "HighVolatility" | "PinBar" | "EmaCross" | "MacdCross" | "MacdCrossUp" | "MacdCrossDown" | "Touch200" | "Reverse200"; export interface EventHandler { onBuy: (candle: Order, reason: string) => void; onSell: (candle: Order, reason: string) => void; onEvent: ( eventType: EventType, { candle, analysis }: { candle: Candle; analysis: Analysis } ) => void; } export class IndicatorService { constructor() {} analyze(candles: Candle[], wave: Wave): Analysis { if (!wave) { wave = { symbol: "", interval: "", trend: "Bullish", numberTouchEma: 0, numberMacdCrossUp: 0, numberMacdCrossDown: 0, lowOrHighPrice: 0, }; } 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 bb = BollingerBands.calculate({ period: 20, stdDev: 2, values: close, reversedInput: true, }); const candle = candles[0]; const analysis: Analysis = { symbol: wave.symbol, interval: wave.interval, emaDirection: "", macdDirection: "", isMacdCrossUp: false, isMacdCrossDown: false, isEmaCrossUp: false, isEmaCrossDown: false, isPinBar: false, isHighVolatility: false, isBuy: false, isSell: false, isTouch200: false, isReverse200: false, isOverBbUpper: false, isUnderBbLower: false, lowHight: wave.lowOrHighPrice, numberTouch200: wave.numberTouchEma, numberMacdCrossUp: wave.numberMacdCrossUp, numberMacdCrossDown: wave.numberMacdCrossDown, isMacdUpper: false, isMacdLower: false, currentBB: { upper: 0, middle: 0, lower: 0, }, }; analysis.currentBB.upper = bb[0].upper; analysis.currentBB.middle = bb[0].middle; analysis.currentBB.lower = bb[0].lower; if (candles[0].high > analysis.currentBB.upper) { analysis.isOverBbUpper = true; } if (candles[0].low < analysis.currentBB.lower) { analysis.isUnderBbLower = true; } if (ema34[0] > ema200[0]) { if (wave.trend === "Bearish") { analysis.numberTouch200 = 0; analysis.numberMacdCrossDown = 0; analysis.numberMacdCrossUp = 1; } analysis.emaDirection = "Bullish"; } else { if (wave.trend === "Bullish") { analysis.numberTouch200 = 0; analysis.numberMacdCrossDown = 1; analysis.numberMacdCrossUp = 0; } analysis.emaDirection = "Bearish"; } if (macd[0].MACD! > macd[0].signal!) { analysis.macdDirection = "Bullish"; } else { analysis.macdDirection = "Bearish"; } if (analysis.emaDirection === "Bullish") { if (analysis.lowHight < candle.high) { analysis.lowHight = candle.high; } } else { if (analysis.lowHight > candle.low) { analysis.lowHight = candle.low; } } if (macd[0].MACD! > 0) { analysis.isMacdUpper = true; } else { analysis.isMacdLower = true; } if (macd[0].MACD! > macd[0].signal! && macd[1].MACD! < macd[1].signal!) { analysis.isMacdCrossUp = true; analysis.numberMacdCrossUp++; } else if ( macd[0].MACD! < macd[0].signal! && macd[1].MACD! > macd[1].signal! ) { analysis.isMacdCrossDown = true; analysis.numberMacdCrossDown++; } if (ema34[0] > ema200[0] && ema34[1] < ema200[1]) { analysis.isEmaCrossUp = true; } else if (ema34[0] < ema200[0] && ema34[1] > ema200[1]) { analysis.isEmaCrossDown = true; } if (isPinBar(candle).isPinBar) { analysis.isPinBar = true; } if (isHighVolatilityCandle(candles.slice(0, 50).reverse())) { analysis.isHighVolatility = true; } if ( candle.low < ema200[0] && candle.high > ema200[0] && candle.close > ema200[0] && candle.open > ema200[0] && analysis.emaDirection === "Bullish" ) { analysis.numberTouch200++; analysis.isTouch200 = true; } if ( candle.high > ema200[0] && candle.low < ema200[0] && candle.close < ema200[0] && candle.open < ema200[0] && analysis.emaDirection === "Bearish" ) { analysis.numberTouch200++; analysis.isTouch200 = true; } if ( candle.open < ema200[0] && candle.close > ema200[0] && analysis.emaDirection === "Bullish" ) { const candlesCheck = [candles[3], candles[2], candles[1], candles[0]]; const ema200Check = [ema200[3], ema200[2], ema200[1], ema200[0]]; const candleCheck = candlesCheck.find( (c, i) => c.close < ema200Check[i] && c.open > ema200Check[i] ); if (candleCheck) { analysis.numberTouch200++; analysis.isReverse200 = true; } } if ( candle.open > ema200[0] && candle.close < ema200[0] && analysis.emaDirection === "Bearish" ) { const candlesCheck = [candles[3], candles[2], candles[1], candles[0]]; const ema200Check = [ema200[3], ema200[2], ema200[1], ema200[0]]; const candleCheck = candlesCheck.find( (c, i) => c.close > ema200Check[i] && c.open < ema200Check[i] ); if (candleCheck) { analysis.numberTouch200++; analysis.isReverse200 = true; } } console.log(analysis); return analysis; } makeOrder( analysis: Analysis, candles: Candle[], side: "buy" | "sell" ): Order { const last10Candles = candles.slice(0, 12); const lowestPrice = last10Candles.reduce( (min, c) => Math.min(min, c.low), Number.MAX_SAFE_INTEGER ); const highestPrice = last10Candles.reduce( (max, c) => Math.max(max, c.high), Number.MIN_SAFE_INTEGER ); let entry = candles[0].close; if (side === "buy") { if ( analysis.currentBB.upper < Math.max(candles[0].close, candles[0].open) ) { entry = analysis.currentBB.upper; } if ( analysis.currentBB.lower < candles[0].open && analysis.currentBB.lower > candles[0].close ) { entry = analysis.currentBB.lower; } } if (side === "sell") { if ( analysis.currentBB.lower > Math.min(candles[0].close, candles[0].open) ) { entry = analysis.currentBB.lower; } if ( analysis.currentBB.upper > candles[0].open && analysis.currentBB.upper < candles[0].close ) { entry = analysis.currentBB.upper; } } const order: Order = { symbol: analysis.symbol, interval: analysis.interval, side, entry, stopLoss: side === "buy" ? lowestPrice : highestPrice, volume: 1, }; return order; } handleEvent( analysis: Analysis, candles: Candle[], eventHandler: EventHandler ) { if ( (analysis.isTouch200 || analysis.isReverse200) && analysis.emaDirection === "Bullish" ) { const order = this.makeOrder(analysis, candles, "buy"); eventHandler.onBuy(order, "Follow trend EMA Touch 200"); return; } if ( (analysis.isTouch200 || analysis.isReverse200) && analysis.emaDirection === "Bearish" ) { const order = this.makeOrder(analysis, candles, "sell"); eventHandler.onSell(order, "Follow trend EMA Touch 200"); return; } if (analysis.isMacdCrossUp && analysis.emaDirection === "Bullish") { const order = this.makeOrder(analysis, candles, "buy"); eventHandler.onBuy(order, "Follow trend MACD Cross Up"); return; } if (analysis.isMacdCrossDown && analysis.emaDirection === "Bearish") { const order = this.makeOrder(analysis, candles, "sell"); eventHandler.onSell(order, "Follow trend MACD Cross Down"); return; } if ( analysis.isMacdCrossUp && analysis.isMacdLower && analysis.numberMacdCrossUp > 2 ) { const order = this.makeOrder(analysis, candles, "buy"); eventHandler.onBuy(order, "Counter trend rủi ro cao MACD Cross Up"); } if ( analysis.isMacdCrossDown && analysis.isMacdUpper && analysis.numberMacdCrossDown > 2 ) { const order = this.makeOrder(analysis, candles, "sell"); eventHandler.onSell(order, "Counter trend rủi ro cao MACD Cross Down"); } if (analysis.isHighVolatility) { eventHandler.onEvent("HighVolatility", { candle: candles[0], analysis }); } } /** * Lấy bản ghi wave theo symbol và interval */ async getWave(symbol: string, interval: string) { const { data, error } = await supabase .from("waves") .select("*") .eq("symbol", symbol) .eq("interval", interval) .single(); if (error) return null; return data; } /** * Update hoặc Insert wave theo symbol và interval */ async upsertWave(wave: Wave) { const { data, error } = await supabase .from("waves") .upsert([wave], { onConflict: "symbol,interval" }); if (error) { console.error(error); } return data; } }