update refactor
This commit is contained in:
parent
01abc7e446
commit
55ad0607f3
39 changed files with 4304 additions and 584 deletions
8
src/api/candles.ts
Normal file
8
src/api/candles.ts
Normal 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
10
src/api/orders.ts
Normal 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
8
src/api/positions.ts
Normal 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
9
src/api/user.ts
Normal 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
27
src/app.ts
Normal 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
0
src/dao/analysis.ts
Normal file
10
src/dao/candles.ts
Normal file
10
src/dao/candles.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export interface Candle {
|
||||
time: string;
|
||||
timestamp: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
turnover: number;
|
||||
}
|
||||
11
src/handlers/candleHandler.ts
Normal file
11
src/handlers/candleHandler.ts
Normal 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);
|
||||
};
|
||||
21
src/handlers/orderHandler.ts
Normal file
21
src/handlers/orderHandler.ts
Normal 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);
|
||||
};
|
||||
8
src/handlers/positionHandler.ts
Normal file
8
src/handlers/positionHandler.ts
Normal 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);
|
||||
};
|
||||
14
src/handlers/userHandler.ts
Normal file
14
src/handlers/userHandler.ts
Normal 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);
|
||||
};
|
||||
26
src/schedule/candleAnalysisSchedule.ts
Normal file
26
src/schedule/candleAnalysisSchedule.ts
Normal 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');
|
||||
});
|
||||
69
src/services/bybitService.ts
Normal file
69
src/services/bybitService.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/services/indicatorService.ts
Normal file
27
src/services/indicatorService.ts
Normal 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] };
|
||||
}
|
||||
22
src/services/messageService.ts
Normal file
22
src/services/messageService.ts
Normal 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 };
|
||||
}
|
||||
16
src/services/supabaseService.ts
Normal file
16
src/services/supabaseService.ts
Normal 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' };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue