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

1831
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@
"dotenv": "^17.0.1",
"express": "^4.18.2",
"node-schedule": "^2.1.1",
"node-telegram-bot-api": "^0.66.0",
"swagger-ui-express": "^5.0.1",
"technicalindicators": "^3.0.0",
"yamljs": "^0.3.0"
@ -30,6 +31,7 @@
"@types/express": "^5.0.3",
"@types/node": "^24.0.10",
"@types/node-schedule": "^2.1.7",
"@types/node-telegram-bot-api": "^0.64.9",
"@types/swagger-ui-express": "^4.1.8",
"@types/yamljs": "^0.2.34",
"nodemon": "^3.1.10",

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);
}

21
test.ts
View file

@ -7,10 +7,11 @@ import {
isPinBar,
} from "./src/helpers/candles";
import { analyzeCandlesJob } from "./src/schedule/candleAnalysisSchedule";
import { log } from "console";
const bybitService = new BybitService(
process.env.BYBIT_API_KEY!,
process.env.BYBIT_API_SECRET!,
'dqGCPAJzLKoTRfgCMq',
'aDYziLWN2jvdWNuz4QhWrM1O65JZ5f1NtEUO',
false
);
@ -20,5 +21,19 @@ function toTimestamp(strTime: string): number {
(async () => {
// await analyzeCandlesJob("ETHUSDT", "15", toTimestamp("2025-07-08 23:59:59"));
await analyzeCandlesJob("ETHUSDT", "15");
// await analyzeCandlesJob("ETHUSDT", "15");
// const res = await bybitService.createOrder({
// category: "linear",
// symbol: "ETHUSDT",
// side: "Buy",
// orderType: "Limit",
// price: "3146.81",
// qty: "1",
// takeProfit: "3200",
// stopLoss: "3000",
// });
// console.log(res);
const balance = await bybitService.getBalance();
console.log(balance.result.list[0].totalEquity);
})();