This commit is contained in:
KienVT9 2025-07-11 17:28:40 +07:00
parent e45c62215e
commit d8f8b04943
15 changed files with 458 additions and 162 deletions

View file

@ -1,11 +1,33 @@
import { EMA, MACD } from "technicalindicators";
import { Candle } from "../dao/candles";
import { analyzeCandleSequence } from "../helpers/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 interface EventHandler {
onBuy: (candle: Order, reason: string) => void;
onSell: (candle: Order, reason: string) => void;
onEvent: (data: any, eventType: string) => void;
}
export class IndicatorService {
constructor() {}
analyze(candles: Candle[]): { ema34: number; ema200: number; macd: any } {
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,
@ -27,28 +49,213 @@ export class IndicatorService {
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,
lowHight: wave.lowOrHighPrice,
numberTouch200: wave.numberTouchEma,
numberMacdCrossUp: wave.numberMacdCrossUp,
numberMacdCrossDown: wave.numberMacdCrossDown,
isMacdUpper: false,
isMacdLower: false,
};
if (ema34[0] > ema200[0]) {
if (wave.trend === "Bearish") {
analysis.numberTouch200 = 0;
}
analysis.emaDirection = "Bullish";
} else {
if (wave.trend === "Bullish") {
analysis.numberTouch200 = 0;
}
analysis.emaDirection = "Bearish";
}
if (macd[0].MACD! > macd[0].signal!) {
if (wave.trend === "Bearish") {
analysis.numberMacdCrossUp = 0;
analysis.numberMacdCrossDown = 0;
}
analysis.macdDirection = "Bullish";
} else {
if (wave.trend === "Bullish") {
analysis.numberMacdCrossDown = 0;
analysis.numberMacdCrossUp = 0;
}
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.close > ema200[0] &&
candle.open > ema200[0] &&
analysis.emaDirection === "Bullish"
) {
console.log("Buy");
analysis.numberTouch200++;
analysis.isTouch200 = true;
}
if (
candle.high > ema200[0] &&
candle.low < ema200[0] &&
candle.close < ema200[0]
candle.close < ema200[0] &&
candle.open < ema200[0] &&
analysis.emaDirection === "Bearish"
) {
console.log("Sell");
analysis.numberTouch200++;
analysis.isTouch200 = true;
}
const candleAnalysis = analyzeCandleSequence([
candles[2],
candles[1],
candles[0],
]);
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;
}
}
return { ema34: ema34[0], ema200: ema200[0], macd: macd[0] };
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);
const order: Order = {
symbol: analysis.symbol,
side,
entry: candles[0].close,
stopLoss: side === "buy" ? lowestPrice : highestPrice,
volume: 1,
};
return order;
}
handleEvent(analysis: Analysis, candles: Candle[], eventHandler: EventHandler) {
if (analysis.isTouch200 && analysis.emaDirection === "Bullish") {
const order = this.makeOrder(analysis, candles, "buy");
eventHandler.onBuy(order, "Follow trend EMA Touch 200");
}
if (analysis.isTouch200 && analysis.emaDirection === "Bearish") {
const order = this.makeOrder(analysis, candles, "sell");
eventHandler.onSell(order, "Follow trend EMA Touch 200");
}
if (analysis.isMacdCrossUp && analysis.isMacdUpper) {
const order = this.makeOrder(analysis, candles, "buy");
eventHandler.onBuy(order, "Follow trend MACD Cross Up");
}
if (analysis.isMacdCrossDown && analysis.isMacdLower) {
const order = this.makeOrder(analysis, candles, "sell");
eventHandler.onSell(order, "Follow trend MACD Cross Down");
}
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");
}
}
/**
* Lấy bản ghi wave theo symbol 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 interval
*/
async upsertWave(wave: Wave) {
const { data, error } = await supabase
.from('waves')
.upsert([
wave
], { onConflict: 'symbol,interval' });
if (error) {
console.error(error);
}
return data;
}
}