121 lines
3.7 KiB
JavaScript
121 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Finnhub 실시간 TQQQ/QQQ 모니터
|
|
* Usage: FINNHUB_API_KEY=your_key node realtime-tqqq-monitor.js
|
|
*/
|
|
|
|
const WebSocket = require('ws');
|
|
|
|
const FINNHUB_API_KEY = process.env.FINNHUB_API_KEY || 'd62ho41r01qlugeq3ge0d62ho41r01qlugeq3geg';
|
|
|
|
if (!FINNHUB_API_KEY) {
|
|
console.error('❌ Error: FINNHUB_API_KEY가 설정되지 않았습니다.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const socket = new WebSocket(`wss://ws.finnhub.io?token=${FINNHUB_API_KEY}`);
|
|
|
|
const prices = {
|
|
TQQQ: null,
|
|
QQQ: null,
|
|
};
|
|
|
|
const STOP_LOSS_PERCENT = -10; // -10% 손절선
|
|
let initialPortfolio = 9000; // $9,000
|
|
|
|
socket.on('open', () => {
|
|
console.log('✅ Finnhub WebSocket 연결 성공!');
|
|
console.log('📊 실시간 모니터링 시작...\n');
|
|
|
|
// TQQQ, QQQ 구독
|
|
socket.send(JSON.stringify({ type: 'subscribe', symbol: 'TQQQ' }));
|
|
socket.send(JSON.stringify({ type: 'subscribe', symbol: 'QQQ' }));
|
|
});
|
|
|
|
socket.on('message', (data) => {
|
|
try {
|
|
const message = JSON.parse(data);
|
|
|
|
if (message.type === 'trade' && message.data) {
|
|
message.data.forEach(trade => {
|
|
const symbol = trade.s;
|
|
const price = trade.p;
|
|
const volume = trade.v;
|
|
const timestamp = new Date(trade.t).toLocaleTimeString('ko-KR');
|
|
|
|
// 가격 업데이트
|
|
if (symbol === 'TQQQ' || symbol === 'QQQ') {
|
|
const prevPrice = prices[symbol];
|
|
prices[symbol] = price;
|
|
|
|
const change = prevPrice ? ((price - prevPrice) / prevPrice * 100).toFixed(2) : '0.00';
|
|
const changeIcon = parseFloat(change) >= 0 ? '📈' : '📉';
|
|
|
|
console.log(`${changeIcon} ${symbol}: $${price.toFixed(2)} | 변동: ${change}% | 거래량: ${volume.toLocaleString()} | ${timestamp}`);
|
|
|
|
// 손절선 체크 (TQQQ만)
|
|
if (symbol === 'TQQQ') {
|
|
checkStopLoss(price);
|
|
checkTargets(price);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error parsing message:', error.message);
|
|
}
|
|
});
|
|
|
|
socket.on('error', (error) => {
|
|
console.error('❌ WebSocket Error:', error.message);
|
|
});
|
|
|
|
socket.on('close', () => {
|
|
console.log('🔌 WebSocket 연결 종료');
|
|
});
|
|
|
|
// 손절선 체크
|
|
function checkStopLoss(currentPrice) {
|
|
// 예시: $46 평단가 기준
|
|
const avgPrice = 46;
|
|
const stopLossPrice = avgPrice * (1 + STOP_LOSS_PERCENT / 100);
|
|
const currentLoss = ((currentPrice - avgPrice) / avgPrice * 100).toFixed(2);
|
|
|
|
if (currentPrice <= stopLossPrice) {
|
|
console.log('\n🚨🚨🚨 손절선 터짐! 🚨🚨🚨');
|
|
console.log(`현재가: $${currentPrice.toFixed(2)}`);
|
|
console.log(`손절선: $${stopLossPrice.toFixed(2)}`);
|
|
console.log(`손실: ${currentLoss}%`);
|
|
console.log('⚠️ 즉시 전량 매도 필요!\n');
|
|
} else if (currentPrice <= stopLossPrice * 1.02) {
|
|
console.log(`\n⚠️ 경고: 손절선 2% 근접 ($${currentPrice.toFixed(2)} vs $${stopLossPrice.toFixed(2)})\n`);
|
|
}
|
|
}
|
|
|
|
// 목표가 체크
|
|
function checkTargets(currentPrice) {
|
|
const avgPrice = 46;
|
|
const targets = [
|
|
{ price: 50, name: '1차 목표', action: '30% 매도' },
|
|
{ price: 54, name: '2차 목표 (복구)', action: '50% 매도' },
|
|
{ price: 58, name: '3차 목표', action: '전량 매도' },
|
|
];
|
|
|
|
targets.forEach(target => {
|
|
if (currentPrice >= target.price && currentPrice <= target.price * 1.01) {
|
|
const profit = ((currentPrice - avgPrice) / avgPrice * 100).toFixed(2);
|
|
console.log(`\n🎯 ${target.name} 도달!`);
|
|
console.log(`현재가: $${currentPrice.toFixed(2)} | 수익: +${profit}%`);
|
|
console.log(`액션: ${target.action}\n`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Ctrl+C로 종료 시
|
|
process.on('SIGINT', () => {
|
|
console.log('\n👋 모니터링 종료...');
|
|
socket.close();
|
|
process.exit(0);
|
|
});
|