mirror of
https://git.hmsn.ink/coin/bot.git
synced 2026-03-19 15:55:01 +09:00
first
This commit is contained in:
239
index.js
Normal file
239
index.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import ccxt from 'ccxt';
|
||||
import WebSocket from 'ws';
|
||||
import fs from 'fs';
|
||||
import { aiSignal } from './strategy.js';
|
||||
import logger from './logger.js';
|
||||
|
||||
const cfg = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
||||
const ex = new ccxt.bybit({
|
||||
apiKey: '',
|
||||
secret: '',
|
||||
options: { defaultType: 'swap' }
|
||||
});
|
||||
|
||||
let bars = [];
|
||||
let balance = cfg.startBalance;
|
||||
let position = null;
|
||||
let trades = [];
|
||||
let curPrice = null;
|
||||
|
||||
async function bootstrap() {
|
||||
try {
|
||||
const ohlcv = await ex.fetchOHLCV(cfg.cctxSymbol, cfg.ohlcvInterval, undefined, 200);
|
||||
bars = ohlcv.map(c => ({
|
||||
t: c[0],
|
||||
o: c[1],
|
||||
h: c[2],
|
||||
l: c[3],
|
||||
c: c[4],
|
||||
v: c[5]
|
||||
}));
|
||||
|
||||
logger.info(`Bootstrapped ${bars.length} bars | Balance: ${balance}`);
|
||||
} catch (error) {
|
||||
logger.critical('초기화 중 심각한 오류 발생', error, {
|
||||
stage: 'bootstrap'
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function initWs() {
|
||||
try {
|
||||
const ws = new WebSocket(`wss://stream.bybit.com/v5/public/linear`);
|
||||
|
||||
ws.on('open', () => {
|
||||
logger.info('WebSocket 연결 성공');
|
||||
ws.send(JSON.stringify({
|
||||
op: 'subscribe',
|
||||
args: [
|
||||
`kline.${cfg.interval}.${cfg.symbol}`,
|
||||
`tickers.${cfg.symbol}`,
|
||||
]
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', (msg) => {
|
||||
try {
|
||||
const m = JSON.parse(msg);
|
||||
if (m.op !== 'subscribe') {
|
||||
if (m.topic?.startsWith('tickers')) {
|
||||
const t = m.data;
|
||||
if (t) {
|
||||
if(t.lastPrice) {
|
||||
curPrice = +t.lastPrice;
|
||||
// logger.debug('현재 가격 업데이트', { curPrice });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m.topic?.startsWith('kline')) {
|
||||
const k = m.data[0];
|
||||
if (!k || !k.confirm) return;
|
||||
|
||||
const bar = {
|
||||
t: +k.start,
|
||||
o: +k.open,
|
||||
h: +k.high,
|
||||
l: +k.low,
|
||||
c: +k.close,
|
||||
v: +k.volume
|
||||
};
|
||||
|
||||
bars.push(bar);
|
||||
if (bars.length > 300) bars.shift();
|
||||
|
||||
logger.debug('새로운 분봉 수신', {
|
||||
timestamp: new Date(bar.t).toISOString(),
|
||||
open: bar.o,
|
||||
high: bar.h,
|
||||
low: bar.l,
|
||||
close: bar.c,
|
||||
volume: bar.v
|
||||
});
|
||||
|
||||
onNewBar(bar);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('WebSocket 메시지 처리 중 오류', e, {
|
||||
rawMessage: msg.toString().substr(0, 100) + '...'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
logger.error('WebSocket 연결 오류', error);
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
logger.warn('WebSocket 연결 종료', {
|
||||
code,
|
||||
reason: reason.toString()
|
||||
});
|
||||
// 재연결 시도 로직 추가 가능
|
||||
});
|
||||
} catch (error) {
|
||||
logger.critical('WebSocket 초기화 실패', error);
|
||||
setTimeout(initWs, 5000); // 5초 후 재시도
|
||||
}
|
||||
}
|
||||
|
||||
async function onNewBar(bar) {
|
||||
logger.debug(`새로운 ${cfg.interval}분 봉 수신`, {
|
||||
timestamp: new Date(bar.t).toISOString(),
|
||||
open: bar.o,
|
||||
high: bar.h,
|
||||
low: bar.l,
|
||||
close: bar.c,
|
||||
volume: bar.v
|
||||
});
|
||||
|
||||
|
||||
if (position) {
|
||||
logger.debug(`진입 보류: 이미 포지션 존재`, {
|
||||
position: {
|
||||
side: position.side,
|
||||
entry: position.entry,
|
||||
qty: position.qty
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sig = await aiSignal(bars);
|
||||
if(sig.side !== 'HOLD') enterMarket(sig);
|
||||
}
|
||||
|
||||
function enterMarket(sig) {
|
||||
|
||||
const qty = ((balance * cfg.leverage) / curPrice).toFixed(4);
|
||||
|
||||
const notional = curPrice * +qty;
|
||||
const fee = notional * cfg.feeTaker / 100;
|
||||
balance -= fee;
|
||||
|
||||
position = {
|
||||
id: `m${Date.now()}`,
|
||||
side: sig.side,
|
||||
entry: curPrice,
|
||||
qty: +qty,
|
||||
sl: sig.sl,
|
||||
tp: sig.tp,
|
||||
openTime: Date.now(),
|
||||
fee,
|
||||
initialBalance: balance + fee
|
||||
};
|
||||
|
||||
logger.info(`[FILLED] ${sig.side} 시장가 @${curPrice} qty=${qty}`, { position });
|
||||
}
|
||||
|
||||
|
||||
function checkExit() {
|
||||
if (!position || curPrice === null) return;
|
||||
|
||||
const { side, entry, qty, sl, tp } = position;
|
||||
let exit = null;
|
||||
|
||||
if (side === 'LONG') {
|
||||
if (curPrice >= tp) exit = 'TP';
|
||||
if (curPrice <= sl) exit = 'SL';
|
||||
} else {
|
||||
if (curPrice <= tp) exit = 'TP';
|
||||
if (curPrice >= sl) exit = 'SL';
|
||||
}
|
||||
|
||||
if (!exit) return;
|
||||
|
||||
const pnl = (side === 'LONG' ? curPrice - entry : entry - curPrice) * qty;
|
||||
const fee = qty * curPrice * (cfg.feeTaker / 100);
|
||||
const netPnl = pnl - fee;
|
||||
balance += netPnl;
|
||||
|
||||
// 음수 잔고 방지를 위한 체크
|
||||
if (balance < 0) {
|
||||
logger.critical('음수 잔고 발생! 시스템 중지 필요', {
|
||||
balanceBefore: balance - netPnl,
|
||||
pnl,
|
||||
fee,
|
||||
netPnl,
|
||||
position
|
||||
});
|
||||
balance = 0; // 안전장치
|
||||
}
|
||||
|
||||
const tradeRecord = {
|
||||
...position,
|
||||
closed: true,
|
||||
exitPrice: curPrice,
|
||||
exitReason: exit,
|
||||
pnl: netPnl,
|
||||
closeTime: Date.now(),
|
||||
finalBalance: balance,
|
||||
leverage: cfg.leverage
|
||||
};
|
||||
|
||||
trades.push(tradeRecord);
|
||||
fs.writeFileSync(cfg.logPath, JSON.stringify(trades, null, 2));
|
||||
|
||||
logger.info(`[EXIT] ${exit} @${curPrice.toFixed(4)} | PnL=${netPnl.toFixed(2)} | Balance=${balance.toFixed(2)}`, {
|
||||
trade: tradeRecord,
|
||||
market: { currentPrice: curPrice }
|
||||
});
|
||||
|
||||
// 종료된 포지션 로깅
|
||||
logger.logPosition(tradeRecord, `포지션 종료 (${exit})`);
|
||||
|
||||
position = null;
|
||||
}
|
||||
|
||||
// 시스템 시작
|
||||
try {
|
||||
await bootstrap();
|
||||
initWs();
|
||||
setInterval(checkExit, 1000);
|
||||
} catch (error) {
|
||||
logger.critical('시스템 시작 중 심각한 오류', error);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user