This commit is contained in:
KienVT9 2025-07-16 15:37:26 +07:00
parent 9cde60bb9c
commit c35d84cf07
11 changed files with 2109 additions and 68 deletions

View file

@ -15,6 +15,8 @@ export interface Analysis {
isSell: boolean;
isTouch200: boolean;
isReverse200: boolean;
isOverBbUpper: boolean;
isUnderBbLower: boolean;
lowHight: number;
numberTouch200: number;
numberMacdCrossUp: number;

View file

@ -1,5 +1,6 @@
export interface Order {
symbol: string;
interval: string;
side: "buy" | "sell";
entry: number;
volume: number;

View file

@ -5,7 +5,7 @@ const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BY
export const createOrder = async (req: Request, res: Response) => {
// TODO: Lấy client từ credential
const order = await bybitService.createOrder(req.body);
const order = await bybitService.submitOrder(req.body);
res.json(order);
};

View file

@ -5,52 +5,23 @@ import { KlineIntervalV3 } from 'bybit-api';
import { sendLarkMessage } from '../services/messageService';
import { Candle } from '../dao/candles';
import { Order } from '../dao/order';
import { EventHandler } from '../services/indicatorService';
const indicatorService = new IndicatorService();
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!, false);
function sendMessage(message: string) {
sendLarkMessage('oc_f9b2e8f0309ecab0c94e3e134b0ddd29', message);
}
// Hàm thực hiện phân tích nến
export async function analyzeCandlesJob(symbol: string, interval: KlineIntervalV3, end?: number) {
export async function analyzeCandlesJob(symbol: string, interval: KlineIntervalV3, eventHandler?: EventHandler, end?: number) {
// TODO: Lấy client thật nếu cần
const candles = await bybitService.getCandles({ symbol, interval, category: 'linear', limit: 500, end });
const wave = await indicatorService.getWave(symbol, interval);
console.log(wave);
const analysis= indicatorService.analyze(candles, wave);
indicatorService.handleEvent(analysis, candles, {
onBuy: (order: Order, reason: string) => {
sendMessage(`Buy ${symbol} ${interval}M ${reason}
symbol: ${order.symbol}
side: ${order.side}
entry: ${order.entry}
stopLoss: ${order.stopLoss}
volume: ${order.volume}
reason: ${reason}
`);
console.log(`Buy ${symbol} ${interval}M ${reason} ${candles[0].time}`);
},
onSell: (order: Order, reason: string) => {
sendMessage(`Sell ${symbol} ${interval}M ${reason}
symbol: ${order.symbol}
side: ${order.side}
entry: ${order.entry}
stopLoss: ${order.stopLoss}
volume: ${order.volume}
reason: ${reason}
`);
console.log(`Sell ${symbol} ${interval}M ${reason} ${candles[0].time}`);
},
onEvent: (data: any, eventType: string) => {
sendMessage(`${eventType} ${symbol} ${interval}M ${data.close}`)
console.log(`${eventType} ${symbol} ${interval}M ${JSON.stringify(data)}`);
},
});
if (eventHandler) {
indicatorService.handleEvent(analysis, candles, eventHandler);
}
analysis.symbol = symbol;
analysis.interval = interval;
console.log(analysis);
await indicatorService.upsertWave({
symbol,
interval,
@ -62,7 +33,7 @@ export async function analyzeCandlesJob(symbol: string, interval: KlineIntervalV
});
}
export function createCandleAnalysisSchedule(symbol: string, interval: KlineIntervalV3) {
export function createCandleAnalysisSchedule(symbol: string, interval: KlineIntervalV3, eventHandler?: EventHandler) {
const rule = new schedule.RecurrenceRule();
rule.tz = 'Asia/Ho_Chi_Minh';
switch (interval) {
@ -89,6 +60,6 @@ export function createCandleAnalysisSchedule(symbol: string, interval: KlineInte
}
rule.second = 59;
schedule.scheduleJob(rule, () => {
analyzeCandlesJob(symbol, interval);
analyzeCandlesJob(symbol, interval, eventHandler);
});
}

View file

@ -1,8 +1,186 @@
import { createCandleAnalysisSchedule } from "./candleAnalysisSchedule";
import { EventHandler, EventType } from "../services/indicatorService";
import { Order } from "../dao/order";
import { sendLarkMessage } from "../services/messageService";
import { BybitService } from "src/services/bybitService";
import { Analysis } from "src/dao/analysis";
createCandleAnalysisSchedule("ETHUSDT", "15");
createCandleAnalysisSchedule("BTCUSDT", "15");
createCandleAnalysisSchedule("ETHUSDT", "60");
createCandleAnalysisSchedule("BTCUSDT", "60");
createCandleAnalysisSchedule("ETHUSDT", "240");
createCandleAnalysisSchedule("BTCUSDT", "240");
function sendMessage(message: string) {
sendLarkMessage("oc_f9b2e8f0309ecab0c94e3e134b0ddd29", message);
}
const bybitService = new BybitService(
process.env.BYBIT_API_KEY!,
process.env.BYBIT_API_SECRET!,
false
);
const eventHandlerFuture: EventHandler = {
onBuy: (order: Order, reason: string) => {
sendMessage(`Future Buy ${order.symbol} ${order.interval}M
symbol: ${order.symbol}
side: ${order.side}
entry: ${order.entry}
stopLoss: ${order.stopLoss}
volume: ${order.volume}
reason: ${reason}
`);
bybitService.createOrderFuture(0.1, 25, {
category: "linear",
symbol: order.symbol,
side: "Buy",
orderType: "Limit",
price: Number(order.entry).toFixed(2),
qty: '1',
});
console.log(
`Buy ${order.symbol} ${order.interval}M ${reason} ${order.entry}`
);
},
onSell: (order: Order, reason: string) => {
sendMessage(`Future Sell ${order.symbol} ${order.interval}M
symbol: ${order.symbol}
side: ${order.side}
entry: ${order.entry}
stopLoss: ${order.stopLoss}
volume: ${order.volume}
reason: ${reason}
`);
bybitService.createOrderFuture(0.1, 25, {
category: "linear",
symbol: order.symbol,
side: "Sell",
orderType: "Limit",
price: Number(order.entry).toFixed(2),
qty: '1',
});
console.log(
`Sell ${order.symbol} ${order.interval}M ${reason} ${order.entry}`
);
},
onEvent: async (eventType: EventType, analysis: Analysis) => {
if (eventType === "HighVolatility") {
const positions = await bybitService.listPositions({
symbol: analysis.symbol,
});
if (positions.length > 0) {
const position = positions[0];
if (position.side === "Buy" && analysis.isOverBbUpper) {
const halfSize = (Number(position.size) / 2).toFixed(2);
await bybitService.submitOrder({
category: "linear",
symbol: analysis.symbol,
side: "Sell",
orderType: "Limit",
price: Number(analysis.currentBB.upper).toFixed(2),
qty: halfSize,
});
sendMessage(`Future Căt nửa ${analysis.symbol} ${analysis.interval}M ${analysis.currentBB.upper} ${halfSize}`);
}
if (position.side === "Sell" && analysis.isUnderBbLower) {
const halfSize = (Number(position.size) / 2).toFixed(2);
await bybitService.submitOrder({
category: "linear",
symbol: analysis.symbol,
side: "Buy",
orderType: "Limit",
price: Number(analysis.currentBB.lower).toFixed(2),
qty: halfSize,
});
sendMessage(`Future Căt nửa ${analysis.symbol} ${analysis.interval}M ${analysis.currentBB.lower} ${halfSize}`);
}
}
}
},
};
const eventHandlerSpot: EventHandler = {
onBuy: (order: Order, reason: string) => {
sendMessage(`Spot Buy ${order.symbol} ${order.interval}M
symbol: ${order.symbol}
side: ${order.side}
entry: ${order.entry}
stopLoss: ${order.stopLoss}
volume: ${order.volume}
reason: ${reason}
`);
bybitService.createOrder(0.1, {
category: "spot",
symbol: order.symbol,
side: "Buy",
orderType: "Limit",
price: Number(order.entry).toFixed(2),
qty: '1',
});
console.log(
`Buy ${order.symbol} ${order.interval}M ${reason} ${order.entry}`
);
},
onSell: (order: Order, reason: string) => {
sendMessage(`Spot Sell ${order.symbol} ${order.interval}M
symbol: ${order.symbol}
side: ${order.side}
entry: ${order.entry}
stopLoss: ${order.stopLoss}
volume: ${order.volume}
reason: ${reason}
`);
},
onEvent: async (eventType: EventType, analysis: Analysis) => {
},
};
const eventHandlerNotification: EventHandler = {
onBuy: (order: Order, reason: string) => {
sendMessage(`Warning Buy ${order.symbol} ${order.interval}M
symbol: ${order.symbol}
side: ${order.side}
entry: ${order.entry}
stopLoss: ${order.stopLoss}
volume: ${order.volume}
reason: ${reason}
`);
console.log(
`Buy ${order.symbol} ${order.interval}M ${reason} ${order.entry}`
);
},
onSell: (order: Order, reason: string) => {
sendMessage(`Warning Sell ${order.symbol} ${order.interval}M
symbol: ${order.symbol}
side: ${order.side}
entry: ${order.entry}
stopLoss: ${order.stopLoss}
volume: ${order.volume}
reason: ${reason}
`);
},
onEvent: async (eventType: EventType, analysis: Analysis) => {
},
};
createCandleAnalysisSchedule("ETHUSDT", "15", eventHandlerFuture);
createCandleAnalysisSchedule("BTCUSDT", "15", eventHandlerFuture);
createCandleAnalysisSchedule("ETHUSDT", "60", eventHandlerNotification);
createCandleAnalysisSchedule("BTCUSDT", "60", eventHandlerNotification);
// Spot
createCandleAnalysisSchedule("ETHUSDT", "240", eventHandlerNotification);
createCandleAnalysisSchedule("BTCUSDT", "240", eventHandlerNotification);
createCandleAnalysisSchedule("SOLUSDT", "240", eventHandlerNotification);
createCandleAnalysisSchedule("ARBUSDT", "240", eventHandlerNotification);
createCandleAnalysisSchedule("SUIUSDT", "240", eventHandlerNotification);
createCandleAnalysisSchedule("APTUSDT", "240", eventHandlerNotification);
createCandleAnalysisSchedule("ETHUSDT", "D", eventHandlerSpot);
createCandleAnalysisSchedule("BTCUSDT", "D", eventHandlerSpot);
createCandleAnalysisSchedule("SOLUSDT", "D", eventHandlerSpot);
createCandleAnalysisSchedule("ARBUSDT", "D", eventHandlerSpot);
createCandleAnalysisSchedule("SUIUSDT", "D", eventHandlerSpot);
createCandleAnalysisSchedule("APTUSDT", "D", eventHandlerSpot);

View file

@ -1,4 +1,4 @@
import { CancelOrderParamsV5, GetAccountOrdersParamsV5, GetKlineParamsV5, OrderParamsV5, RestClientV5 } from 'bybit-api';
import { CancelOrderParamsV5, GetAccountOrdersParamsV5, GetKlineParamsV5, OrderParamsV5, PositionV5, RestClientV5 } from 'bybit-api';
import { Candle } from '../dao/candles';
export class BybitService {
private client: RestClientV5;
@ -11,7 +11,7 @@ export class BybitService {
});
}
async createOrder(orderData: OrderParamsV5): Promise<any> {
async submitOrder(orderData: OrderParamsV5): Promise<any> {
try {
const res = await this.client.submitOrder(orderData);
return { success: true, data: res };
@ -20,6 +20,56 @@ export class BybitService {
}
}
async createOrder(vl: number, orderData: OrderParamsV5): Promise<any> {
try {
const balance = await this.client.getWalletBalance({
accountType: "UNIFIED",
coin: "USDT"
});
console.log(balance);
const total = balance.result.list[0].totalEquity
const volume = Number(total) * vl
const qty = volume / Number(orderData.price)
orderData.qty = qty.toFixed(2);
const res = await this.client.submitOrder(orderData);
return { success: true, data: res };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async createOrderFuture(vl: number, leverage: number, orderData: OrderParamsV5): Promise<any> {
try {
const balance = await this.client.getWalletBalance({
accountType: "UNIFIED",
coin: "USDT"
});
await this.client.setLeverage({
category: "linear",
symbol: orderData.symbol,
buyLeverage: leverage.toString(),
sellLeverage: leverage.toString(),
});
const total = balance.result.list[0].totalEquity
const volume = Number(total) * vl * leverage
const qty = volume / Number(orderData.price)
orderData.qty = qty.toFixed(2);
const res = await this.client.submitOrder(orderData);
return { success: true, data: res };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async getBalance(): Promise<any> {
const balance = await this.client.getWalletBalance({
accountType: "UNIFIED",
coin: "USDT"
});
return balance;
}
async listOrders(params: GetAccountOrdersParamsV5): Promise<any[]> {
try {
const res = await this.client.getActiveOrders(params);
@ -38,7 +88,7 @@ export class BybitService {
}
}
async listPositions(params: any): Promise<any[]> {
async listPositions(params: any): Promise<PositionV5[]> {
try {
const res = await this.client.getPositionInfo(params);
return res.result?.list || [];

View file

@ -11,10 +11,12 @@ 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: (data: any, eventType: string) => void;
onEvent: (eventType: EventType, analysis: Analysis) => void;
}
export class IndicatorService {
@ -77,6 +79,8 @@ export class IndicatorService {
isSell: false,
isTouch200: false,
isReverse200: false,
isOverBbUpper: false,
isUnderBbLower: false,
lowHight: wave.lowOrHighPrice,
numberTouch200: wave.numberTouchEma,
numberMacdCrossUp: wave.numberMacdCrossUp,
@ -94,6 +98,14 @@ export class IndicatorService {
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;
@ -241,6 +253,7 @@ export class IndicatorService {
const order: Order = {
symbol: analysis.symbol,
interval: analysis.interval,
side,
entry,
stopLoss: side === "buy" ? lowestPrice : highestPrice,
@ -287,6 +300,10 @@ export class IndicatorService {
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", analysis);
}
}
/**

View file

@ -1,4 +1,5 @@
import { Client } from '@larksuiteoapi/node-sdk';
import TelegramBot from 'node-telegram-bot-api';
export async function sendLarkMessage(receiveId: string, message: string): Promise<any> {
console.log(process.env.LARK_APP_ID!, process.env.LARK_APP_SECRET!);
@ -19,4 +20,9 @@ export async function sendLarkMessage(receiveId: string, message: string): Promi
});
return { success: true, data: res.data };
}
}
export function sendTelegramMessage(chatId: string, message: string) {
const bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN!);
bot.sendMessage(chatId, message);
}