Skill: smeuse-dev/upbit-trading-skill (version 1.0.0)

--- name: upbit-trading version: 1.0.0 description: Upbit 실시간 트레이딩 봇 - GLM AI 분석, 기술지표, 자동매매 author: smeuseBot price: 29.99 tags: [trading, crypto, upbit, automation, korean] --- # Upbit Trading Bot 🚀 AI 기반 실시간 암호화폐 트레이딩 봇 ## Features - 📊 **기술 지표**: RSI, MACD, Bollinger Bands, MA/EMA - 🤖 **AI 분석**: GLM-4.7 실시간 시장 분석 - ⚡ **10초 모니터링**: 빠른 가격 체크 - 🎯 **자동 목표/손절**: 설정 가능한 TP/SL - 📱 **텔레그램 알림**: 실시간 이벤트 알림 ## Setup 1. Upbit API 키 발급 (https://upbit.com/mypage/open_api_management) 2. 환경변수 설정: ```bash cp .env.example .env # UPBIT_ACCESS_KEY, UPBIT_SECRET_KEY 입력 ``` 3. 실행: ```bash node realtime-bot.js ``` ## Requirements - Node.js 18+ - Upbit 계정 & API 키 - (선택) GLM API 키 for AI 분석 ## Files - `realtime-bot.js` - 메인 봇 - `indicators.js` - 기술 지표 계산 - `analyze.js` - 시장 분석 - `balance.js` - 잔고 확인 ## License MIT - 자유롭게 사용 및 수정 가능

path: balance.js
content: #!/usr/bin/env node
// 업비트 잔고 조회

require('dotenv').config({ path: __dirname + '/.env' });
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');

const ACCESS_KEY = process.env.UPBIT_ACCESS_KEY;
const SECRET_KEY = process.env.UPBIT_SECRET_KEY;

async function getBalance() {
  const payload = {
    access_key: ACCESS_KEY,
    nonce: uuidv4(),
  };
  
  const [REDACTED_SECRET], SECRET_KEY);
  
  try {
    const response = await axios.get('https://api.upbit.com/v1/accounts', {
      headers: { Authorization: `Bearer ${token}` }
    });
    
    console.log('=== 업비트 자산 현황 ===\n');
    
    let totalKRW = 0;
    
    for (const account of response.data) {
      const currency = account.currency;
      const balance = parseFloat(account.balance);
      const locked = parseFloat(account.locked);
      const avgBuyPrice = parseFloat(account.avg_buy_price);
      
      if (balance > 0 || locked > 0) {
        console.log(`[${currency}]`);
        console.log(`  잔고: ${balance.toLocaleString()}`);
        if (locked > 0) console.log(`  거래중: ${locked.toLocaleString()}`);
        if (avgBuyPrice > 0) console.log(`  평균매수가: ${avgBuyPrice.toLocaleString()} KRW`);
        console.log('');
        
        if (currency === 'KRW') {
          totalKRW += balance;
        }
      }
    }
    
    return response.data;
  } catch (error) {
    console.error('Error:', error.response?.data || error.message);
    throw error;
  }
}

getBalance();


path: indicators.js
content: #!/usr/bin/env node
// 기술적 지표 계산

/**
 * RSI (Relative Strength Index) 계산
 * @param {number[]} prices - 종가 배열
 * @param {number} period - 기간 (기본 14)
 */
function calculateRSI(prices, period = 14) {
  if (prices.length < period + 1) return null;
  
  let gains = 0;
  let losses = 0;
  
  // 첫 번째 평균 계산
  for (let i = 1; i <= period; i++) {
    const change = prices[i] - prices[i - 1];
    if (change > 0) gains += change;
    else losses -= change;
  }
  
  let avgGain = gains / period;
  let avgLoss = losses / period;
  
  // 나머지 기간 Smoothed 계산
  for (let i = period + 1; i < prices.length; i++) {
    const change = prices[i] - prices[i - 1];
    if (change > 0) {
      avgGain = (avgGain * (period - 1) + change) / period;
      avgLoss = (avgLoss * (period - 1)) / period;
    } else {
      avgGain = (avgGain * (period - 1)) / period;
      avgLoss = (avgLoss * (period - 1) - change) / period;
    }
  }
  
  if (avgLoss === 0) return 100;
  const rs = avgGain / avgLoss;
  return 100 - (100 / (1 + rs));
}

/**
 * 이동평균 계산
 */
function calculateMA(prices, period) {
  if (prices.length < period) return null;
  const slice = prices.slice(-period);
  return slice.reduce((a, b) => a + b, 0) / period;
}

/**
 * EMA (지수이동평균) 계산
 */
function calculateEMA(prices, period) {
  if (prices.length < period) return null;
  
  const multiplier = 2 / (period + 1);
  let ema = prices.slice(0, period).reduce((a, b) => a + b, 0) / period;
  
  for (let i = period; i < prices.length; i++) {
    ema = (prices[i] - ema) * multiplier + ema;
  }
  
  return ema;
}

/**
 * MACD 계산
 */
function calculateMACD(prices, fastPeriod = 12, slowPeriod = 26, signalPeriod = 9) {
  const fastEMA = calculateEMA(prices, fastPeriod);
  const slowEMA = calculateEMA(prices, slowPeriod);
  
  if (!fastEMA || !slowEMA) return null;
  
  const macd = fastEMA - slowEMA;
  // Signal line은 MACD의 EMA인데, 여기선 단순화
  
  return {
    macd,
    signal: null, // 추후 구현
    histogram: null
  };
}

/**
 * 볼린저 밴드 계산
 */
function calculateBollingerBands(prices, period = 20, multiplier = 2) {
  if (prices.length < period) return null;
  
  const slice = prices.slice(-period);
  const ma = slice.reduce((a, b) => a + b, 0) / period;
  
  const squaredDiffs = slice.map(p => Math.pow(p - ma, 2));
  const variance = squaredDiffs.reduce((a, b) => a + b, 0) / period;
  const stdDev = Math.sqrt(variance);
  
  return {
    upper: ma + (multiplier * stdDev),
    middle: ma,
    lower: ma - (multiplier * stdDev)
  };
}

/**
 * 변동성 돌파 전략 신호
 * @param {Object} candle - {open, high, low, close}
 * @param {number} prevRange - 전일 고가-저가
 * @param {number} k - 변동성 계수 (기본 0.5)
 */
function volatilityBreakout(candle, prevRange, k = 0.5) {
  const targetPrice = candle.open + (prevRange * k);
  return {
    targetPrice,
    signal: candle.close > targetPrice ? 'BUY' : 'HOLD',
    breakout: candle.high > targetPrice
  };
}

module.exports = {
  calculateRSI,
  calculateMA,
  calculateEMA,
  calculateMACD,
  calculateBollingerBands,
  volatilityBreakout
};

// CLI 테스트
if (require.main === module) {
  const testPrices = [44, 44.5, 43.5, 44.5, 44, 43.5, 44, 44.5, 44, 43.5, 44.5, 45, 45.5, 46, 45.5, 46, 46.5, 47, 46.5, 47];
  
  console.log('=== 기술적 지표 테스트 ===');
  console.log('테스트 데이터:', testPrices.length, '개');
  console.log('RSI(14):', calculateRSI(testPrices, 14)?.toFixed(2));
  console.log('MA(5):', calculateMA(testPrices, 5)?.toFixed(2));
  console.log('EMA(5):', calculateEMA(testPrices, 5)?.toFixed(2));
  console.log('BB(20):', calculateBollingerBands(testPrices, 20));
}


path: realtime-bot.js
content: #!/usr/bin/env node
/**
 * Upbit 실시간 모니터링 봇
 * - 10초마다 가격 체크
 * - 30초마다 GLM-4.7로 분석
 * - 이벤트 발생 시 events.json에 기록
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// 설정
const CONFIG = {
  priceCheckInterval: 10000,  // 10초
  analysisInterval: 30000,    // 30초
  targetProfit: 0.05,         // +5%
  stopLoss: -0.05,            // -5%
};

const POSITIONS_FILE = path.join(__dirname, 'positions.json');
const EVENTS_FILE = path.join(__dirname, 'events.json');
const TRADE_LOG = path.join(__dirname, 'trade_log.json');

// 유틸리티
function loadJSON(file, defaultValue = []) {
  try {
    return JSON.parse(fs.readFileSync(file, 'utf8'));
  } catch {
    return defaultValue;
  }
}

function saveJSON(file, data) {
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
}

function log(msg) {
  const timestamp = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' });
  console.log(`[${timestamp}] ${msg}`);
}

// Upbit API (네이티브 fetch 사용)
async function getPrice(market) {
  const res = await fetch(`https://api.upbit.com/v1/ticker?markets=${market}`);
  const data = await res.json();
  return data[0]?.trade_price || null;
}

// GLM 분석 호출
function askGLM(prompt) {
  try {
    const result = execSync(
      `cd ${__dirname}/../zai && ./ask.sh "${prompt.replace(/"/g, '\\"')}" glm-4.7`,
      { encoding: 'utf8', timeout: 30000 }
    );
    return result.trim();
  } catch (err) {
    log(`GLM 호출 실패: ${err.message}`);
    return null;
  }
}

// 이벤트 추가
function addEvent(event) {
  const events = loadJSON(EVENTS_FILE, []);
  events.push({
    ...event,
    timestamp: new Date().toISOString(),
    processed: false
  });
  saveJSON(EVENTS_FILE, events);
  log(`📢 이벤트 추가: ${event.type} - ${event.message}`);
}

// 포지션 체크
async function checkPositions() {
  const data = loadJSON(POSITIONS_FILE, { positions: [] });
  const positions = data.positions || [];
  
  for (const pos of positions) {
    if (pos.status !== 'open') continue;
    
    const currentPrice = await getPrice(pos.market);
    if (!currentPrice) continue;
    
    const entryPrice = pos.entryPrice || pos.avgPrice;
    const pnlPercent = (currentPrice - entryPrice) / entryPrice;
    
    log(`${pos.market}: ${currentPrice}원 (${(pnlPercent * 100).toFixed(2)}%)`);
    
    // 목표 도달
    if (pnlPercent >= CONFIG.targetProfit) {
      addEvent({
        type: 'TARGET_HIT',
        market: pos.market,
        entryPrice,
        currentPrice,
        pnlPercent,
        message: `🎯 ${pos.market} 목표 도달! +${(pnlPercent * 100).toFixed(2)}%`
      });
    }
    // 손절 도달
    else if (pnlPercent <= CONFIG.stopLoss) {
      addEvent({
        type: 'STOPLOSS_HIT',
        market: pos.market,
        entryPrice,
        currentPrice,
        pnlPercent,
        message: `🚨 ${pos.market} 손절 도달! ${(pnlPercent * 100).toFixed(2)}%`
      });
    }
  }
}

// GLM 시장 분석
async function analyzeMarket() {
  const data = loadJSON(POSITIONS_FILE, { positions: [] });
  const positions = data.positions || [];
  const openPositions = positions.filter(p => p.status === 'open');
  
  if (openPositions.length === 0) {
    log('열린 포지션 없음 - 새 기회 탐색');
    
    // 새 매수 기회 분석
    const prompt = `당신은 암호화폐 트레이딩 봇입니다. 현재 KRW 잔고가 약 600원으로 매우 적습니다.
다음 중 하나만 답하세요:
1. WAIT - 잔고가 너무 적어서 대기
2. WATCH:마켓코드 - 주목할 코인 (예: WATCH:KRW-BTC)

현재 시장: Fear & Greed Index 14 (극도의 공포)
응답 형식: WAIT 또는 WATCH:마켓코드`;
    
    const response = askGLM(prompt);
    if (response) {
      log(`GLM 분석: ${response}`);
      if (response.includes('WATCH:')) {
        const market = response.split('WATCH:')[1]?.trim().split(/\s/)[0];
        if (market) {
          addEvent({
            type: 'WATCH_SIGNAL',
            market,
            message: `👀 GLM 추천 주목 코인: ${market}`
          });
        }
      }
    }
  } else {
    // 기존 포지션 분석
    for (const pos of openPositions) {
      const currentPrice = await getPrice(pos.market);
      const entryPrice = pos.entryPrice || pos.avgPrice;
      const pnlPercent = (currentPrice - entryPrice) / entryPrice;
      
      const prompt = `당신은 암호화폐 트레이딩 봇입니다.
포지션: ${pos.market}
진입가: ${entryPrice}원
현재가: ${currentPrice}원
손익: ${(pnlPercent * 100).toFixed(2)}%
목표: +5%, 손절: -5%

다음 중 하나만 답하세요:
1. HOLD - 유지
2. SELL_NOW - 즉시 매도 (목표/손절 전이라도)
3. ADJUST:새목표,새손절 - 목표/손절 조정

응답 형식: HOLD, SELL_NOW, 또는 ADJUST:5,-3`;

      const response = askGLM(prompt);
      if (response) {
        log(`GLM 포지션 분석 (${pos.market}): ${response}`);
        
        if (response.includes('SELL_NOW')) {
          addEvent({
            type: 'GLM_SELL_SIGNAL',
            market: pos.market,
            currentPrice,
            pnlPercent,
            message: `⚠️ GLM 매도 권고: ${pos.market} (${(pnlPercent * 100).toFixed(2)}%)`
          });
        } else if (response.includes('ADJUST:')) {
          const adjustMatch = response.match(/ADJUST:([^,]+),(.+)/);
          if (adjustMatch) {
            addEvent({
              type: 'GLM_ADJUST',
              market: pos.market,
              newTarget: parseFloat(adjustMatch[1]),
              newStopLoss: parseFloat(adjustMatch[2]),
              message: `📊 GLM 조정 권고: ${pos.market} 목표 ${adjustMatch[1]}%, 손절 ${adjustMatch[2]}%`
            });
          }
        }
      }
    }
  }
}

// 메인 루프
let analysisCounter = 0;

async function mainLoop() {
  log('=== 가격 체크 ===');
  await checkPositions();
  
  analysisCounter += CONFIG.priceCheckInterval;
  
  // 30초마다 GLM 분석
  if (analysisCounter >= CONFIG.analysisInterval) {
    log('=== GLM 분석 ===');
    await analyzeMarket();
    analysisCounter = 0;
  }
}

// 시작
log('🤖 Upbit 실시간 봇 시작');
log(`설정: 가격 체크 ${CONFIG.priceCheckInterval/1000}초, GLM 분석 ${CONFIG.analysisInterval/1000}초`);

// 즉시 한 번 실행
mainLoop();

// 주기적 실행
setInterval(mainLoop, CONFIG.priceCheckInterval);

// 종료 처리
process.on('SIGINT', () => {
  log('봇 종료');
  process.exit(0);
});


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)