mirror of
https://git.hmsn.ink/coin/bot.git
synced 2026-03-19 15:55:01 +09:00
first
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.idea
|
.idea
|
||||||
|
logs
|
||||||
12
config.json
Normal file
12
config.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"cctxSymbol": "ETH/USDT:USDT",
|
||||||
|
"symbol": "ETHUSDT",
|
||||||
|
"startBalance": 10000,
|
||||||
|
"orderSizeQuote": 100,
|
||||||
|
"interval": 60,
|
||||||
|
"ohlcvInterval": "1h",
|
||||||
|
"leverage": 5,
|
||||||
|
"feeTaker": 0.055,
|
||||||
|
"pendingCancelTime": 1200000,
|
||||||
|
"logPath": "./tradeLog.json"
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
180
logger.js
Normal file
180
logger.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// 로그 디렉토리 생성
|
||||||
|
const logDir = './logs';
|
||||||
|
if (!fs.existsSync(logDir)) {
|
||||||
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그 레벨 정의
|
||||||
|
const logLevels = {
|
||||||
|
DEBUG: 0,
|
||||||
|
INFO: 1,
|
||||||
|
WARN: 2,
|
||||||
|
ERROR: 3,
|
||||||
|
CRITICAL: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
const logLevelNames = Object.keys(logLevels);
|
||||||
|
const currentLogLevel = logLevels.info; // 설정에 따라 변경 가능
|
||||||
|
|
||||||
|
// 타임스탬프 생성
|
||||||
|
const getTimestamp = () => new Date().toISOString();
|
||||||
|
|
||||||
|
// 호출자 정보 추출 (ESM 호환)
|
||||||
|
const getCallerInfo = () => {
|
||||||
|
const err = new Error();
|
||||||
|
const stack = err.stack.split('\n');
|
||||||
|
|
||||||
|
// 스택에서 호출자 위치 찾기
|
||||||
|
let callerIndex = 2; // 기본값 (0: Error, 1: getCallerInfo, 2: logger function)
|
||||||
|
|
||||||
|
// 'at '로 시작하는 라인 찾기
|
||||||
|
for (let i = 2; i < stack.length; i++) {
|
||||||
|
if (stack[i].includes('at ')) {
|
||||||
|
callerIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack.length > callerIndex) {
|
||||||
|
const callerLine = stack[callerIndex].trim();
|
||||||
|
|
||||||
|
// 파일 경로 추출 (Windows와 Unix 경로 모두 처리)
|
||||||
|
const fileMatch = callerLine.match(/\((.*?):\d+:\d+\)/) ||
|
||||||
|
callerLine.match(/(.*?):\d+:\d+$/);
|
||||||
|
|
||||||
|
if (fileMatch && fileMatch[1]) {
|
||||||
|
const filePath = fileMatch[1];
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 로그 포맷팅
|
||||||
|
const formatLog = (level, message, metadata = {}) => {
|
||||||
|
const timestamp = getTimestamp();
|
||||||
|
const callerInfo = getCallerInfo();
|
||||||
|
const levelName = logLevelNames[level];
|
||||||
|
|
||||||
|
let logMessage = `[${levelName}] ${timestamp} [${callerInfo}] ${message}`;
|
||||||
|
|
||||||
|
if (Object.keys(metadata).length > 0) {
|
||||||
|
try {
|
||||||
|
logMessage += `\n Meta: ${JSON.stringify(metadata, null, 2)}`;
|
||||||
|
} catch (e) {
|
||||||
|
logMessage += `\n Meta: [Circular or invalid object]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return logMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 로그 파일 쓰기
|
||||||
|
const writeLog = (logMessage, level) => {
|
||||||
|
if (level < currentLogLevel) return;
|
||||||
|
|
||||||
|
// 콘솔 출력
|
||||||
|
if (level >= logLevels.ERROR) {
|
||||||
|
console.error(logMessage);
|
||||||
|
} else {
|
||||||
|
console.log(logMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 로깅
|
||||||
|
try {
|
||||||
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
const logFile = path.join(logDir, `trading-${date}.log`);
|
||||||
|
fs.appendFileSync(logFile, logMessage + '\n');
|
||||||
|
} catch (e) {
|
||||||
|
// 파일 로깅 실패 시 콘솔에 경고
|
||||||
|
console.error(`[LOGGER] Failed to write log file: ${e.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 로거 인스턴스 생성
|
||||||
|
const createLogger = () => {
|
||||||
|
return {
|
||||||
|
debug: (message, metadata = {}) => {
|
||||||
|
const logMessage = formatLog(logLevels.DEBUG, message, metadata);
|
||||||
|
writeLog(logMessage, logLevels.DEBUG);
|
||||||
|
},
|
||||||
|
|
||||||
|
info: (message, metadata = {}) => {
|
||||||
|
const logMessage = formatLog(logLevels.INFO, message, metadata);
|
||||||
|
writeLog(logMessage, logLevels.INFO);
|
||||||
|
},
|
||||||
|
|
||||||
|
warn: (message, metadata = {}) => {
|
||||||
|
const logMessage = formatLog(logLevels.WARN, message, metadata);
|
||||||
|
writeLog(logMessage, logLevels.WARN);
|
||||||
|
},
|
||||||
|
|
||||||
|
error: (message, error = null, metadata = {}) => {
|
||||||
|
const errorMetadata = { ...metadata };
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
errorMetadata.error = {
|
||||||
|
message: error.message || 'Unknown error',
|
||||||
|
stack: error.stack ?
|
||||||
|
error.stack.split('\n').slice(0, 5).join('\n') :
|
||||||
|
'No stack trace'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const logMessage = formatLog(logLevels.ERROR, message, errorMetadata);
|
||||||
|
writeLog(logMessage, logLevels.ERROR);
|
||||||
|
},
|
||||||
|
|
||||||
|
critical: (message, error = null, metadata = {}) => {
|
||||||
|
const errorMetadata = { ...metadata };
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
errorMetadata.error = {
|
||||||
|
message: error.message || 'Unknown critical error',
|
||||||
|
stack: error.stack || 'No stack trace'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const logMessage = formatLog(logLevels.CRITICAL, message, errorMetadata);
|
||||||
|
writeLog(logMessage, logLevels.CRITICAL);
|
||||||
|
},
|
||||||
|
|
||||||
|
logPosition: (position, message) => {
|
||||||
|
if (!position) return;
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
position: {
|
||||||
|
id: position.id,
|
||||||
|
side: position.side,
|
||||||
|
entry: position.entry,
|
||||||
|
qty: position.qty,
|
||||||
|
sl: position.sl,
|
||||||
|
tp: position.tp,
|
||||||
|
openTime: position.openTime ?
|
||||||
|
new Date(position.openTime).toISOString() : 'N/A'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (position.closed) {
|
||||||
|
metadata.position.exitPrice = position.exitPrice;
|
||||||
|
metadata.position.exitReason = position.exitReason;
|
||||||
|
metadata.position.pnl = position.pnl;
|
||||||
|
metadata.position.closeTime = position.closeTime ?
|
||||||
|
new Date(position.closeTime).toISOString() : 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.info(message, metadata);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 싱글톤 인스턴스 생성
|
||||||
|
const logger = createLogger();
|
||||||
|
|
||||||
|
// 모듈 내보내기
|
||||||
|
export default logger;
|
||||||
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Auto Trade Bot",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "yarn "
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^24.3.0",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"ccxt": "^4.5.2",
|
||||||
|
"fs-extra": "^11.3.1",
|
||||||
|
"openai": "^5.16.0",
|
||||||
|
"regression": "^2.0.1",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
41
strategy.js
Normal file
41
strategy.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// strategy.js
|
||||||
|
import logger from './logger.js';
|
||||||
|
import {OpenAI} from "openai";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
baseURL: 'https://openrouter.ai/api/v1',
|
||||||
|
apiKey: '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',
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function aiSignal(bars) {
|
||||||
|
if (bars.length < 20) return null;
|
||||||
|
// const time = '15분'
|
||||||
|
const time = '1시간'
|
||||||
|
console.log(Date.now())
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: 'qwen/qwen3-235b-a22b-2507',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `당신은 코인 매매 전문 트레이너 입니다.
|
||||||
|
매매 방법은 스윙매매 입니다. 1시간 봉으로 매매 하는 만큼 큰 수익을 얻어야 합니다. 너무 무리 하지 않는 선에서 제어가 필요합니다.
|
||||||
|
아래에 제공 되는 json ${time}봉 데이터를 보고 여러지표(macd, rsi, ema, 다이버전스, ovm, vwap) 등 을 대입하여 포지션, 타점, 스톱로스, 테이크피로핏을 정의 해줘야합니다
|
||||||
|
모든 대답은 간결하게 {side: 포지션, price: 진입가, sl: 스탑로스, tp: 테이크프로핏, resaon: 간단한이유} json 형태로 대답하세요.
|
||||||
|
포지션은 SHORT, LONG, HOLD 포지션이 LONG, SHORT 일때는 전체적인 추세를 보고 진입가 손절 익절 금액을 채워주세요.
|
||||||
|
reason 에는 포지션을 지지하는 이유를 요약 하여 채워주세요.
|
||||||
|
|
||||||
|
t: 시간 o: 시작가 h: 최고가 l: 최저가 c: 종료가 v: 볼륨
|
||||||
|
${JSON.stringify(bars)}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
console.log(Date.now())
|
||||||
|
const msg = completion.choices[0].message.content
|
||||||
|
logger.info(msg);
|
||||||
|
const sig = JSON.parse(msg)
|
||||||
|
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
55
test.js
Normal file
55
test.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { aiSignal } from './strategy.js';
|
||||||
|
import ccxt from "ccxt";
|
||||||
|
import fs from "fs";
|
||||||
|
import logger from "./logger.js";
|
||||||
|
|
||||||
|
const cfg = JSON.parse(fs.readFileSync('./config.json', 'utf8'))
|
||||||
|
|
||||||
|
const ex = new ccxt.bybit({
|
||||||
|
apiKey: '',
|
||||||
|
secret: '',
|
||||||
|
options: { defaultType: 'swap' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const ohlcv = await ex.fetchOHLCV(cfg.cctxSymbol, cfg.ohlcvInterval, undefined, 200);
|
||||||
|
const bars = ohlcv.map(c => ({
|
||||||
|
t: c[0],
|
||||||
|
o: c[1],
|
||||||
|
h: c[2],
|
||||||
|
l: c[3],
|
||||||
|
c: c[4],
|
||||||
|
v: c[5]
|
||||||
|
}));
|
||||||
|
const sig = await aiSignal(bars);
|
||||||
|
// const sig = JSON.parse(msg)
|
||||||
|
console.log(sig)
|
||||||
|
console.log(sig.side)
|
||||||
|
console.log(sig.sl)
|
||||||
|
console.log(sig.tp)
|
||||||
|
console.log(sig.reason)
|
||||||
|
let position= {}
|
||||||
|
let balance = 10000
|
||||||
|
const curPrice = sig.price
|
||||||
|
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 });
|
||||||
|
}
|
||||||
226
yarn.lock
Normal file
226
yarn.lock
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/node@^24.3.0":
|
||||||
|
version "24.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec"
|
||||||
|
integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~7.10.0"
|
||||||
|
|
||||||
|
asynckit@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
|
axios@^1.11.0:
|
||||||
|
version "1.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6"
|
||||||
|
integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.15.6"
|
||||||
|
form-data "^4.0.4"
|
||||||
|
proxy-from-env "^1.1.0"
|
||||||
|
|
||||||
|
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||||
|
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
ccxt@^4.5.2:
|
||||||
|
version "4.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ccxt/-/ccxt-4.5.2.tgz#a777d64f8122040229c37ad7adae3b08c75c2a84"
|
||||||
|
integrity sha512-IY21AVv8E+qJJPQFt2NNGiEFOygtl7dAOwojIh0Lxyz/+L7/q0m54Swjk/ALHCuvGQeBWQBgYrn+U6b45Zx9BQ==
|
||||||
|
dependencies:
|
||||||
|
ws "^8.8.1"
|
||||||
|
|
||||||
|
combined-stream@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
|
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||||
|
dependencies:
|
||||||
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
delayed-stream@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
|
dunder-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||||
|
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
|
||||||
|
es-define-property@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||||
|
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||||
|
|
||||||
|
es-errors@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||||
|
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||||
|
|
||||||
|
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||||
|
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
|
||||||
|
es-set-tostringtag@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
|
||||||
|
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||||
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
get-intrinsic "^1.2.6"
|
||||||
|
has-tostringtag "^1.0.2"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
|
||||||
|
follow-redirects@^1.15.6:
|
||||||
|
version "1.15.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
|
||||||
|
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
|
||||||
|
|
||||||
|
form-data@^4.0.4:
|
||||||
|
version "4.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
|
||||||
|
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "^1.0.8"
|
||||||
|
es-set-tostringtag "^2.1.0"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
fs-extra@^11.3.1:
|
||||||
|
version "11.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.1.tgz#ba7a1f97a85f94c6db2e52ff69570db3671d5a74"
|
||||||
|
integrity sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.2.0"
|
||||||
|
jsonfile "^6.0.1"
|
||||||
|
universalify "^2.0.0"
|
||||||
|
|
||||||
|
function-bind@^1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||||
|
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||||
|
|
||||||
|
get-intrinsic@^1.2.6:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||||
|
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers "^1.0.2"
|
||||||
|
es-define-property "^1.0.1"
|
||||||
|
es-errors "^1.3.0"
|
||||||
|
es-object-atoms "^1.1.1"
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
get-proto "^1.0.1"
|
||||||
|
gopd "^1.2.0"
|
||||||
|
has-symbols "^1.1.0"
|
||||||
|
hasown "^2.0.2"
|
||||||
|
math-intrinsics "^1.1.0"
|
||||||
|
|
||||||
|
get-proto@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||||
|
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||||
|
dependencies:
|
||||||
|
dunder-proto "^1.0.1"
|
||||||
|
es-object-atoms "^1.0.0"
|
||||||
|
|
||||||
|
gopd@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||||
|
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||||
|
|
||||||
|
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||||
|
version "4.2.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||||
|
|
||||||
|
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||||
|
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||||
|
|
||||||
|
has-tostringtag@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||||
|
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||||
|
dependencies:
|
||||||
|
has-symbols "^1.0.3"
|
||||||
|
|
||||||
|
hasown@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||||
|
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||||
|
dependencies:
|
||||||
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
jsonfile@^6.0.1:
|
||||||
|
version "6.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62"
|
||||||
|
integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==
|
||||||
|
dependencies:
|
||||||
|
universalify "^2.0.0"
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
|
math-intrinsics@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||||
|
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
version "1.52.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||||
|
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||||
|
|
||||||
|
mime-types@^2.1.12:
|
||||||
|
version "2.1.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||||
|
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||||
|
dependencies:
|
||||||
|
mime-db "1.52.0"
|
||||||
|
|
||||||
|
openai@^5.16.0:
|
||||||
|
version "5.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/openai/-/openai-5.16.0.tgz#a302d4ca92954598c79c72dd199c58994708130d"
|
||||||
|
integrity sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw==
|
||||||
|
|
||||||
|
proxy-from-env@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||||
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
|
regression@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/regression/-/regression-2.0.1.tgz#8d29c3e8224a10850c35e337e85a8b2fac3b0c87"
|
||||||
|
integrity sha512-A4XYsc37dsBaNOgEjkJKzfJlE394IMmUPlI/p3TTI9u3T+2a+eox5Pr/CPUqF0eszeWZJPAc6QkroAhuUpWDJQ==
|
||||||
|
|
||||||
|
undici-types@~7.10.0:
|
||||||
|
version "7.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350"
|
||||||
|
integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==
|
||||||
|
|
||||||
|
universalify@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
||||||
|
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
|
||||||
|
|
||||||
|
ws@^8.18.3, ws@^8.8.1:
|
||||||
|
version "8.18.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
|
||||||
|
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
|
||||||
Reference in New Issue
Block a user