update refactor

This commit is contained in:
KienVT9 2025-07-08 18:03:36 +07:00
parent 01abc7e446
commit 55ad0607f3
39 changed files with 4304 additions and 584 deletions

8
src/api/candles.ts Normal file
View file

@ -0,0 +1,8 @@
import { Router } from 'express';
import * as candleHandler from '../handlers/candleHandler';
const router = Router();
router.get('/:symbol/:interval', candleHandler.analyzeCandles);
export default router;

10
src/api/orders.ts Normal file
View file

@ -0,0 +1,10 @@
import { Router } from 'express';
import * as orderHandler from '../handlers/orderHandler';
const router = Router();
router.post('/', orderHandler.createOrder);
router.get('/', orderHandler.listOrders);
router.delete('/:orderId', orderHandler.cancelOrder);
export default router;

8
src/api/positions.ts Normal file
View file

@ -0,0 +1,8 @@
import { Router } from 'express';
import * as positionHandler from '../handlers/positionHandler';
const router = Router();
router.get('/', positionHandler.listPositions);
export default router;

9
src/api/user.ts Normal file
View file

@ -0,0 +1,9 @@
import { Router } from 'express';
import * as userHandler from '../handlers/userHandler';
const router = Router();
router.post('/credential', userHandler.saveCredential);
router.get('/credential/:userId', userHandler.getCredential);
export default router;

27
src/app.ts Normal file
View file

@ -0,0 +1,27 @@
import 'dotenv/config';
import express, { Application } from 'express';
import userApi from './api/user';
import ordersApi from './api/orders';
import positionsApi from './api/positions';
import candlesApi from './api/candles';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
import './schedule/candleAnalysisSchedule';
const app: Application = express();
app.use(express.json());
app.use('/api/user', userApi);
app.use('/api/orders', ordersApi);
app.use('/api/positions', positionsApi);
app.use('/api/candles', candlesApi);
const swaggerDocument = YAML.load('./openapi.yaml');
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
export default app;

0
src/dao/analysis.ts Normal file
View file

10
src/dao/candles.ts Normal file
View file

@ -0,0 +1,10 @@
export interface Candle {
time: string;
timestamp: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
turnover: number;
}

View file

@ -0,0 +1,11 @@
import { Request, Response } from 'express';
import { BybitService } from '../services/bybitService';
import * as indicatorService from '../services/indicatorService';
export const analyzeCandles = async (req: Request, res: Response) => {
const { symbol, interval } = req.params;
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
const candles = await bybitService.getCandles({ symbol, interval: '5', category: 'linear', limit: 200 });
const analysis = indicatorService.analyze(candles);
res.json(analysis);
};

View file

@ -0,0 +1,21 @@
import { Request, Response } from 'express';
import { BybitService } from '../services/bybitService';
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
export const createOrder = async (req: Request, res: Response) => {
// TODO: Lấy client từ credential
const order = await bybitService.createOrder(req.body);
res.json(order);
};
export const listOrders = async (req: Request, res: Response) => {
const orders = await bybitService.listOrders({ category: 'linear' });
res.json(orders);
};
export const cancelOrder = async (req: Request, res: Response) => {
const { orderId } = req.params;
const result = await bybitService.cancelOrder({ orderId, symbol: 'BTCUSDT', category: 'linear' });
res.json(result);
};

View file

@ -0,0 +1,8 @@
import { Request, Response } from 'express';
import { BybitService } from '../services/bybitService';
export const listPositions = async (req: Request, res: Response) => {
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
const positions = await bybitService.listPositions({ category: 'linear' });
res.json(positions);
};

View file

@ -0,0 +1,14 @@
import { Request, Response } from 'express';
import * as supabaseService from '../services/supabaseService';
export const saveCredential = async (req: Request, res: Response) => {
const { userId, apiKey, apiSecret } = req.body;
const result = await supabaseService.saveBybitCredential(userId, apiKey, apiSecret);
res.json(result);
};
export const getCredential = async (req: Request, res: Response) => {
const { userId } = req.params;
const result = await supabaseService.getBybitCredential(userId);
res.json(result);
};

View file

@ -0,0 +1,26 @@
import schedule from 'node-schedule';
import * as indicatorService from '../services/indicatorService';
import { BybitService } from '../services/bybitService';
import { KlineIntervalV3 } from 'bybit-api';
import { sendLarkMessage } from '../services/messageService';
// Hàm thực hiện phân tích nến
async function analyzeCandlesJob(symbol: string, interval: KlineIntervalV3) {
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!, false);
// TODO: Lấy client thật nếu cần
const candles = await bybitService.getCandles({ symbol, interval, category: 'linear', limit: 200 });
const analysis = indicatorService.analyze(candles);
console.log(`[${new Date().toISOString()}] Phân tích nến ${symbol} ${interval}:`, analysis);
await sendLarkMessage('oc_f9b2e8f0309ecab0c94e3e134b0ddd29', JSON.stringify(analysis));
// Có thể gửi kết quả qua Lark, lưu DB, ...
}
// Lập lịch chạy vào 00:00 mỗi ngày
const rule = new schedule.RecurrenceRule();
rule.minute = [4, 9, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59];
rule.second = 59;
schedule.scheduleJob(rule, () => {
// Có thể lặp qua nhiều symbol/interval nếu muốn
analyzeCandlesJob('BTCUSDT', '5');
});

View file

@ -0,0 +1,69 @@
import { CancelOrderParamsV5, GetAccountOrdersParamsV5, GetKlineParamsV5, OrderParamsV5, RestClientV5 } from 'bybit-api';
import { Candle } from '../dao/candles';
export class BybitService {
private client: RestClientV5;
constructor(apiKey: string, apiSecret: string, testnet: boolean = true) {
this.client = new RestClientV5({
key: apiKey,
secret: apiSecret,
testnet
});
}
async createOrder(orderData: OrderParamsV5): Promise<any> {
try {
const res = await this.client.submitOrder(orderData);
return { success: true, data: res };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async listOrders(params: GetAccountOrdersParamsV5): Promise<any[]> {
try {
const res = await this.client.getActiveOrders(params);
return res.result?.list || [];
} catch (error: any) {
return [];
}
}
async cancelOrder(params: CancelOrderParamsV5): Promise<any> {
try {
const res = await this.client.cancelOrder(params);
return { success: true, data: res };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async listPositions(params: any): Promise<any[]> {
try {
const res = await this.client.getPositionInfo(params);
return res.result?.list || [];
} catch (error: any) {
return [];
}
}
async getCandles(params: GetKlineParamsV5): Promise<Candle[]> {
try {
const res = await this.client.getKline(params);
const list = res.result?.list || [];
// Bybit trả về: [timestamp, open, high, low, close, volume, turnover]
return list.map((item: any[]) => ({
time: new Date(Number(item[0])).toISOString(),
timestamp: Number(item[0]),
open: Number(item[1]),
high: Number(item[2]),
low: Number(item[3]),
close: Number(item[4]),
volume: Number(item[5]),
turnover: Number(item[6])
}));
} catch (error: any) {
return [];
}
}
}

View file

@ -0,0 +1,27 @@
import { EMA, MACD } from 'technicalindicators';
import { Candle } from '../dao/candles';
export function analyze(candles: Candle[]): { ema34: number; ema200: number; macd: any } {
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 candle = candles[0];
if (candle.low < ema200[0] && candle.high > ema200[0] && candle.close > ema200[0]) {
console.log('Buy');
}
if (candle.high > ema200[0] && candle.low < ema200[0] && candle.close < ema200[0]) {
console.log('Sell');
}
return { ema34: ema34[0], ema200: ema200[0], macd: macd[0] };
}

View file

@ -0,0 +1,22 @@
import { Client } from '@larksuiteoapi/node-sdk';
export async function sendLarkMessage(receiveId: string, message: string): Promise<any> {
console.log(process.env.LARK_APP_ID!, process.env.LARK_APP_SECRET!);
const lark = new Client({
appId: process.env.LARK_APP_ID!,
appSecret: process.env.LARK_APP_SECRET!
});
const res = await lark.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: receiveId,
content: JSON.stringify({text: message}),
msg_type: 'text'
}
});
return { success: true, data: res.data };
}

View file

@ -0,0 +1,16 @@
import { createClient, SupabaseClient } from '@supabase/supabase-js';
const supabase: SupabaseClient = createClient(
process.env.SUPABASE_URL || '',
process.env.SUPABASE_KEY || ''
);
export async function saveBybitCredential(userId: string, apiKey: string, apiSecret: string): Promise<any> {
// TODO: Lưu credential vào Supabase
return { success: true, message: 'Saved (mock)' };
}
export async function getBybitCredential(userId: string): Promise<any> {
// TODO: Lấy credential từ Supabase
return { userId, apiKey: 'demo', apiSecret: 'demo' };
}