376 lines
9.7 KiB
TypeScript
376 lines
9.7 KiB
TypeScript
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;
|
|
}
|
|
}
|