update
This commit is contained in:
parent
e45c62215e
commit
d8f8b04943
15 changed files with 458 additions and 162 deletions
18
ecosystem.config.js
Normal file
18
ecosystem.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "ai-trading-sys",
|
||||||
|
script: "node dist/app.js",
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
max_memory_restart: "512M",
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "development",
|
||||||
|
},
|
||||||
|
env_production: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -6,7 +6,7 @@ import positionsApi from './api/positions';
|
||||||
import candlesApi from './api/candles';
|
import candlesApi from './api/candles';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
import YAML from 'yamljs';
|
import YAML from 'yamljs';
|
||||||
import './schedule/candleAnalysisSchedule';
|
import './schedule';
|
||||||
|
|
||||||
|
|
||||||
const app: Application = express();
|
const app: Application = express();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
export interface Analysis {
|
||||||
|
symbol: string;
|
||||||
|
interval: string;
|
||||||
|
emaDirection: string;
|
||||||
|
macdDirection: string;
|
||||||
|
isMacdCrossUp: boolean;
|
||||||
|
isMacdCrossDown: boolean;
|
||||||
|
isMacdUpper: boolean;
|
||||||
|
isMacdLower: boolean;
|
||||||
|
isEmaCrossUp: boolean;
|
||||||
|
isEmaCrossDown: boolean;
|
||||||
|
isPinBar: boolean;
|
||||||
|
isHighVolatility: boolean;
|
||||||
|
isBuy: boolean;
|
||||||
|
isSell: boolean;
|
||||||
|
isTouch200: boolean;
|
||||||
|
isReverse200: boolean;
|
||||||
|
lowHight: number;
|
||||||
|
numberTouch200: number;
|
||||||
|
numberMacdCrossUp: number;
|
||||||
|
numberMacdCrossDown: number;
|
||||||
|
}
|
||||||
9
src/dao/order.ts
Normal file
9
src/dao/order.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export interface Order {
|
||||||
|
symbol: string;
|
||||||
|
side: "buy" | "sell";
|
||||||
|
entry: number;
|
||||||
|
volume: number;
|
||||||
|
leverage?: number;
|
||||||
|
stopLoss?: number;
|
||||||
|
takeProfit?: number;
|
||||||
|
}
|
||||||
9
src/dao/position.ts
Normal file
9
src/dao/position.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export interface Position {
|
||||||
|
symbol: string;
|
||||||
|
side: string;
|
||||||
|
entry: number;
|
||||||
|
leverage: number;
|
||||||
|
volume: number;
|
||||||
|
profit: number;
|
||||||
|
profitPercentage: number;
|
||||||
|
}
|
||||||
10
src/dao/subwave.ts
Normal file
10
src/dao/subwave.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export interface Subwave {
|
||||||
|
symbol: string;
|
||||||
|
interval: string;
|
||||||
|
subwave: string;
|
||||||
|
subwaveDirection: string;
|
||||||
|
subwaveType: string;
|
||||||
|
subwaveLength: number;
|
||||||
|
subwaveStart: number;
|
||||||
|
subwaveEnd: number;
|
||||||
|
}
|
||||||
9
src/dao/wave.ts
Normal file
9
src/dao/wave.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export interface Wave {
|
||||||
|
symbol: string;
|
||||||
|
interval: string;
|
||||||
|
trend: 'Bullish' | 'Bearish';
|
||||||
|
numberTouchEma: number;
|
||||||
|
numberMacdCrossUp: number;
|
||||||
|
numberMacdCrossDown: number;
|
||||||
|
lowOrHighPrice: number;
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { BybitService } from '../services/bybitService';
|
import { BybitService } from '../services/bybitService';
|
||||||
import * as indicatorService from '../services/indicatorService';
|
import { IndicatorService } from '../services/indicatorService';
|
||||||
|
import { Candle } from '../dao/candles';
|
||||||
|
import { KlineIntervalV3 } from 'bybit-api';
|
||||||
|
|
||||||
export const analyzeCandles = async (req: Request, res: Response) => {
|
export const analyzeCandles = async (req: Request, res: Response) => {
|
||||||
const { symbol, interval } = req.params;
|
const { symbol, interval: intervalString } = req.params;
|
||||||
|
const interval = intervalString as KlineIntervalV3;
|
||||||
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
|
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 candles = await bybitService.getCandles({ symbol, interval, category: 'linear', limit: 200 });
|
||||||
const analysis = indicatorService.analyze(candles);
|
const indicatorService = new IndicatorService();
|
||||||
|
const analysis = indicatorService.analyze(candles, {
|
||||||
|
symbol,
|
||||||
|
interval,
|
||||||
|
trend: 'Bullish',
|
||||||
|
numberTouchEma: 0,
|
||||||
|
numberMacdCrossUp: 0,
|
||||||
|
numberMacdCrossDown: 0,
|
||||||
|
lowOrHighPrice: 0,
|
||||||
|
});
|
||||||
res.json(analysis);
|
res.json(analysis);
|
||||||
};
|
};
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
interface Candle {
|
import { Candle } from "../dao/candles";
|
||||||
open: number;
|
|
||||||
close: number;
|
|
||||||
low: number;
|
|
||||||
high: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PatternType =
|
type PatternType =
|
||||||
| "Doji"
|
| "Doji"
|
||||||
|
|
@ -647,3 +642,28 @@ export function isHighVolatilityCandle(candles: Candle[]): boolean {
|
||||||
// A candle is considered high volatility if either its range or its body size (or both) are significantly larger
|
// A candle is considered high volatility if either its range or its body size (or both) are significantly larger
|
||||||
return isRangeHigh || isBodySizeHigh;
|
return isRangeHigh || isBodySizeHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upperShadow(candle: Candle): number {
|
||||||
|
return candle.high - Math.max(candle.open, candle.close);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lowerShadow(candle: Candle): number {
|
||||||
|
return Math.min(candle.open, candle.close) - candle.low;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPinBar(candle: Candle): {isPinBar: boolean, isBullish: boolean, isBearish: boolean} {
|
||||||
|
const bodySize = getBodySize(candle);
|
||||||
|
const totalRange = getTotalRange(candle);
|
||||||
|
const upperShadowSize = upperShadow(candle);
|
||||||
|
const lowerShadowSize = lowerShadow(candle);
|
||||||
|
|
||||||
|
if (bodySize < totalRange * 0.3 && upperShadowSize > bodySize * 2) {
|
||||||
|
return {isPinBar: true, isBullish: true, isBearish: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodySize < totalRange * 0.3 && lowerShadowSize > bodySize * 2) {
|
||||||
|
return {isPinBar: true, isBullish: false, isBearish: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {isPinBar: false, isBullish: false, isBearish: false};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,94 @@
|
||||||
import schedule from 'node-schedule';
|
import schedule from 'node-schedule';
|
||||||
import * as indicatorService from '../services/indicatorService';
|
import { IndicatorService } from '../services/indicatorService';
|
||||||
import { BybitService } from '../services/bybitService';
|
import { BybitService } from '../services/bybitService';
|
||||||
import { KlineIntervalV3 } from 'bybit-api';
|
import { KlineIntervalV3 } from 'bybit-api';
|
||||||
import { sendLarkMessage } from '../services/messageService';
|
import { sendLarkMessage } from '../services/messageService';
|
||||||
|
import { Candle } from '../dao/candles';
|
||||||
|
import { Order } from '../dao/order';
|
||||||
|
|
||||||
// Hàm thực hiện phân tích nến
|
const indicatorService = new IndicatorService();
|
||||||
async function analyzeCandlesJob(symbol: string, interval: KlineIntervalV3) {
|
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!, false);
|
||||||
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
|
function sendMessage(message: string) {
|
||||||
const candles = await bybitService.getCandles({ symbol, interval, category: 'linear', limit: 200 });
|
sendLarkMessage('oc_f9b2e8f0309ecab0c94e3e134b0ddd29', message);
|
||||||
const analysis = indicatorService.analyze(candles);
|
|
||||||
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
|
// Hàm thực hiện phân tích nến
|
||||||
const rule = new schedule.RecurrenceRule();
|
export async function analyzeCandlesJob(symbol: string, interval: KlineIntervalV3, end?: number) {
|
||||||
rule.minute = [4, 9, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59];
|
// TODO: Lấy client thật nếu cần
|
||||||
rule.second = 59;
|
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)}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
analysis.symbol = symbol;
|
||||||
|
analysis.interval = interval;
|
||||||
|
console.log(analysis);
|
||||||
|
await indicatorService.upsertWave({
|
||||||
|
symbol,
|
||||||
|
interval,
|
||||||
|
trend: analysis.emaDirection as 'Bullish' | 'Bearish',
|
||||||
|
numberTouchEma: analysis.numberTouch200,
|
||||||
|
numberMacdCrossUp: analysis.numberMacdCrossUp,
|
||||||
|
numberMacdCrossDown: analysis.numberMacdCrossDown,
|
||||||
|
lowOrHighPrice: analysis.lowHight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
schedule.scheduleJob(rule, () => {
|
export function createCandleAnalysisSchedule(symbol: string, interval: KlineIntervalV3) {
|
||||||
// Có thể lặp qua nhiều symbol/interval nếu muốn
|
const rule = new schedule.RecurrenceRule();
|
||||||
analyzeCandlesJob('BTCUSDT', '5');
|
rule.tz = 'Asia/Ho_Chi_Minh';
|
||||||
});
|
switch (interval) {
|
||||||
|
case '5':
|
||||||
|
rule.minute = [4, 9, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59];
|
||||||
|
break;
|
||||||
|
case '15':
|
||||||
|
rule.minute = [14, 29, 44, 59];
|
||||||
|
break;
|
||||||
|
case '30':
|
||||||
|
rule.minute = [29, 59];
|
||||||
|
break;
|
||||||
|
case '60':
|
||||||
|
rule.minute = [59];
|
||||||
|
break;
|
||||||
|
case '240':
|
||||||
|
rule.minute = [59];
|
||||||
|
rule.hour = [2, 6, 10, 14, 18, 22];
|
||||||
|
break;
|
||||||
|
case 'D':
|
||||||
|
rule.minute = [59];
|
||||||
|
rule.hour = [6];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
rule.second = 59;
|
||||||
|
schedule.scheduleJob(rule, () => {
|
||||||
|
analyzeCandlesJob(symbol, interval);
|
||||||
|
});
|
||||||
|
}
|
||||||
8
src/schedule/index.ts
Normal file
8
src/schedule/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { createCandleAnalysisSchedule } from "./candleAnalysisSchedule";
|
||||||
|
|
||||||
|
createCandleAnalysisSchedule("ETHUSDT", "15");
|
||||||
|
createCandleAnalysisSchedule("BTCUSDT", "15");
|
||||||
|
createCandleAnalysisSchedule("ETHUSDT", "60");
|
||||||
|
createCandleAnalysisSchedule("BTCUSDT", "60");
|
||||||
|
createCandleAnalysisSchedule("ETHUSDT", "240");
|
||||||
|
createCandleAnalysisSchedule("BTCUSDT", "240");
|
||||||
|
|
@ -1,11 +1,33 @@
|
||||||
import { EMA, MACD } from "technicalindicators";
|
import { EMA, MACD } from "technicalindicators";
|
||||||
import { Candle } from "../dao/candles";
|
import { Candle } from "../dao/candles";
|
||||||
import { analyzeCandleSequence } from "../helpers/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 interface EventHandler {
|
||||||
|
onBuy: (candle: Order, reason: string) => void;
|
||||||
|
onSell: (candle: Order, reason: string) => void;
|
||||||
|
onEvent: (data: any, eventType: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export class IndicatorService {
|
export class IndicatorService {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
analyze(candles: Candle[]): { ema34: number; ema200: number; macd: any } {
|
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);
|
let close = candles.map((c) => c.close);
|
||||||
const ema34 = EMA.calculate({
|
const ema34 = EMA.calculate({
|
||||||
period: 34,
|
period: 34,
|
||||||
|
|
@ -27,28 +49,213 @@ export class IndicatorService {
|
||||||
reversedInput: true,
|
reversedInput: true,
|
||||||
});
|
});
|
||||||
const candle = candles[0];
|
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,
|
||||||
|
lowHight: wave.lowOrHighPrice,
|
||||||
|
numberTouch200: wave.numberTouchEma,
|
||||||
|
numberMacdCrossUp: wave.numberMacdCrossUp,
|
||||||
|
numberMacdCrossDown: wave.numberMacdCrossDown,
|
||||||
|
isMacdUpper: false,
|
||||||
|
isMacdLower: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ema34[0] > ema200[0]) {
|
||||||
|
if (wave.trend === "Bearish") {
|
||||||
|
analysis.numberTouch200 = 0;
|
||||||
|
}
|
||||||
|
analysis.emaDirection = "Bullish";
|
||||||
|
} else {
|
||||||
|
if (wave.trend === "Bullish") {
|
||||||
|
analysis.numberTouch200 = 0;
|
||||||
|
}
|
||||||
|
analysis.emaDirection = "Bearish";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (macd[0].MACD! > macd[0].signal!) {
|
||||||
|
if (wave.trend === "Bearish") {
|
||||||
|
analysis.numberMacdCrossUp = 0;
|
||||||
|
analysis.numberMacdCrossDown = 0;
|
||||||
|
}
|
||||||
|
analysis.macdDirection = "Bullish";
|
||||||
|
} else {
|
||||||
|
if (wave.trend === "Bullish") {
|
||||||
|
analysis.numberMacdCrossDown = 0;
|
||||||
|
analysis.numberMacdCrossUp = 0;
|
||||||
|
}
|
||||||
|
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 (
|
if (
|
||||||
candle.low < ema200[0] &&
|
candle.low < ema200[0] &&
|
||||||
candle.high > ema200[0] &&
|
candle.high > ema200[0] &&
|
||||||
candle.close > ema200[0]
|
candle.close > ema200[0] &&
|
||||||
|
candle.open > ema200[0] &&
|
||||||
|
analysis.emaDirection === "Bullish"
|
||||||
) {
|
) {
|
||||||
console.log("Buy");
|
analysis.numberTouch200++;
|
||||||
|
analysis.isTouch200 = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
candle.high > ema200[0] &&
|
candle.high > ema200[0] &&
|
||||||
candle.low < ema200[0] &&
|
candle.low < ema200[0] &&
|
||||||
candle.close < ema200[0]
|
candle.close < ema200[0] &&
|
||||||
|
candle.open < ema200[0] &&
|
||||||
|
analysis.emaDirection === "Bearish"
|
||||||
) {
|
) {
|
||||||
console.log("Sell");
|
analysis.numberTouch200++;
|
||||||
|
analysis.isTouch200 = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const candleAnalysis = analyzeCandleSequence([
|
if (candle.open < ema200[0] && candle.close > ema200[0] && analysis.emaDirection === "Bullish") {
|
||||||
candles[2],
|
const candlesCheck = [candles[3], candles[2], candles[1], candles[0]];
|
||||||
candles[1],
|
const ema200Check = [ema200[3], ema200[2], ema200[1], ema200[0]];
|
||||||
candles[0],
|
const candleCheck = candlesCheck.find(
|
||||||
]);
|
(c, i) => c.close < ema200Check[i] && c.open > ema200Check[i]
|
||||||
|
);
|
||||||
|
if (candleCheck) {
|
||||||
|
analysis.numberTouch200++;
|
||||||
|
analysis.isReverse200 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { ema34: ema34[0], ema200: ema200[0], macd: macd[0] };
|
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);
|
||||||
|
const order: Order = {
|
||||||
|
symbol: analysis.symbol,
|
||||||
|
side,
|
||||||
|
entry: candles[0].close,
|
||||||
|
stopLoss: side === "buy" ? lowestPrice : highestPrice,
|
||||||
|
volume: 1,
|
||||||
|
};
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent(analysis: Analysis, candles: Candle[], eventHandler: EventHandler) {
|
||||||
|
if (analysis.isTouch200 && analysis.emaDirection === "Bullish") {
|
||||||
|
const order = this.makeOrder(analysis, candles, "buy");
|
||||||
|
eventHandler.onBuy(order, "Follow trend EMA Touch 200");
|
||||||
|
}
|
||||||
|
if (analysis.isTouch200 && analysis.emaDirection === "Bearish") {
|
||||||
|
const order = this.makeOrder(analysis, candles, "sell");
|
||||||
|
eventHandler.onSell(order, "Follow trend EMA Touch 200");
|
||||||
|
}
|
||||||
|
if (analysis.isMacdCrossUp && analysis.isMacdUpper) {
|
||||||
|
const order = this.makeOrder(analysis, candles, "buy");
|
||||||
|
eventHandler.onBuy(order, "Follow trend MACD Cross Up");
|
||||||
|
}
|
||||||
|
if (analysis.isMacdCrossDown && analysis.isMacdLower) {
|
||||||
|
const order = this.makeOrder(analysis, candles, "sell");
|
||||||
|
eventHandler.onSell(order, "Follow trend MACD Cross Down");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
const supabase: SupabaseClient = createClient(
|
export const supabase: SupabaseClient = createClient(
|
||||||
process.env.SUPABASE_URL || '',
|
process.env.SUPABASE_URL || '',
|
||||||
process.env.SUPABASE_KEY || ''
|
process.env.SUPABASE_KEY || ''
|
||||||
);
|
);
|
||||||
|
|
|
||||||
20
test.ts
20
test.ts
|
|
@ -4,23 +4,21 @@ import * as indicatorService from "./src/services/indicatorService";
|
||||||
import {
|
import {
|
||||||
analyzeCandleSequence,
|
analyzeCandleSequence,
|
||||||
isHighVolatilityCandle,
|
isHighVolatilityCandle,
|
||||||
|
isPinBar,
|
||||||
} from "./src/helpers/candles";
|
} from "./src/helpers/candles";
|
||||||
|
import { analyzeCandlesJob } from "./src/schedule/candleAnalysisSchedule";
|
||||||
|
|
||||||
const bybitService = new BybitService(
|
const bybitService = new BybitService(
|
||||||
process.env.BYBIT_API_KEY!,
|
process.env.BYBIT_API_KEY!,
|
||||||
process.env.BYBIT_API_SECRET!,
|
process.env.BYBIT_API_SECRET!,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function toTimestamp(strTime: string): number {
|
||||||
|
return new Date(strTime).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const candles = await bybitService.getCandles({
|
// await analyzeCandlesJob("ETHUSDT", "15", toTimestamp("2025-07-08 23:59:59"));
|
||||||
symbol: "BTCUSDT",
|
await analyzeCandlesJob("ETHUSDT", "15");
|
||||||
interval: "240",
|
|
||||||
category: "linear",
|
|
||||||
limit: 500,
|
|
||||||
});
|
|
||||||
console.log(candles[0], candles[1]);
|
|
||||||
const cA = analyzeCandleSequence([candles[2], candles[1], candles[0]]);
|
|
||||||
const isH = isHighVolatilityCandle(candles.reverse());
|
|
||||||
console.log(cA);
|
|
||||||
console.log("isHighVolatilityCandle", isH);
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
125
tsconfig.json
125
tsconfig.json
|
|
@ -1,113 +1,18 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
/* Projects */
|
"rootDir": "src",
|
||||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
"outDir": "dist",
|
||||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
"esModuleInterop": true,
|
||||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
"forceConsistentCasingInFileNames": true,
|
||||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
"strict": true,
|
||||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
"skipLibCheck": true,
|
||||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true,
|
||||||
/* Language and Environment */
|
"declaration": true,
|
||||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
"baseUrl": "./"
|
||||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
},
|
||||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
"include": ["src/**/*"],
|
||||||
// "libReplacement": true, /* Enable lib replacement. */
|
"exclude": ["node_modules", "dist", "ecosystem.config.js", "test.ts"]
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
|
||||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
|
||||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
|
||||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
|
||||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
|
||||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
|
||||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
|
||||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
|
||||||
|
|
||||||
/* Modules */
|
|
||||||
"module": "commonjs", /* Specify what module code is generated. */
|
|
||||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
|
||||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
|
||||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
|
||||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
||||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
|
||||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
|
||||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
|
||||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
|
||||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
|
||||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
|
||||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
|
||||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
|
||||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
|
||||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
|
||||||
|
|
||||||
/* JavaScript Support */
|
|
||||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
|
||||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
|
||||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
|
||||||
|
|
||||||
/* Emit */
|
|
||||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
|
||||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
|
||||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
|
||||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
|
||||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
|
||||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
|
||||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
|
||||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
|
||||||
// "removeComments": true, /* Disable emitting comments. */
|
|
||||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
|
||||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
|
||||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
|
||||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
|
||||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
|
||||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
|
||||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
|
||||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
|
||||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
|
||||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
|
||||||
|
|
||||||
/* Interop Constraints */
|
|
||||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
|
||||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
|
||||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
|
||||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
|
||||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
|
||||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
|
||||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
||||||
|
|
||||||
/* Type Checking */
|
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
|
||||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
|
||||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
|
||||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
|
||||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
|
||||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
|
||||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
|
||||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
|
||||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
|
||||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
|
||||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
|
||||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
|
||||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
|
||||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
|
||||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
|
||||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
|
||||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
|
||||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
|
||||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
|
||||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
|
||||||
|
|
||||||
/* Completeness */
|
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue