Files

200 lines
6.5 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* TQQQ Polygon 실시간 모니터 (크론용)
*
* Polygon REST API로 실시간 가격 조회
* Auto-retry 포함
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
// ============================================================================
// Configuration
// ============================================================================
const CONFIG = {
POLYGON_API_KEY: process.env.POLYGON_API_KEY ||
JSON.parse(fs.readFileSync(path.join(process.env.HOME, '.openclaw/openclaw.json'), 'utf8'))
.env?.POLYGON_API_KEY,
SYMBOL: 'TQQQ',
MAX_RETRIES: 3,
TIMEOUT: 10000, // 10초
// 환율 (실시간 조회 가능하면 업그레이드)
USD_KRW: 1465.09
};
// ============================================================================
// Polygon API 호출
// ============================================================================
function fetchPolygonQuote(symbol) {
return new Promise((resolve, reject) => {
const url = `https://api.polygon.io/v2/last/trade/${symbol}?apiKey=${CONFIG.POLYGON_API_KEY}`;
const req = https.get(url, {
timeout: CONFIG.TIMEOUT
}, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(data);
if (json.status === 'OK' && json.results) {
resolve(json.results);
} else {
reject(new Error(`Polygon API error: ${json.error || 'Unknown error'}`));
}
} catch (error) {
reject(new Error(`JSON parse error: ${error.message}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
});
}
// ============================================================================
// Retry Logic
// ============================================================================
async function fetchWithRetry(symbol, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`🔄 Attempt ${attempt}/${maxRetries}...`);
const result = await fetchPolygonQuote(symbol);
console.log(`✅ Success on attempt ${attempt}`);
return result;
} catch (error) {
lastError = error;
console.error(`❌ Attempt ${attempt} failed: ${error.message}`);
if (attempt < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
console.log(`⏳ Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
// ============================================================================
// Format Output
// ============================================================================
function formatOutput(data) {
const price = data.p; // Last trade price
const timestamp = new Date(data.t / 1000000); // Nanoseconds to milliseconds
const krwPrice = Math.round(price * CONFIG.USD_KRW);
// MEMORY.md에서 포지션 정보 읽기 (실패 시 기본값)
let position = null;
try {
const memoryPath = path.join(process.env.HOME, 'openclaw/MEMORY.md');
const memoryContent = fs.readFileSync(memoryPath, 'utf8');
// "현재 상황:" 섹션 파싱 (간단 정규식)
const statusMatch = memoryContent.match(/ :\s*(.+?)(?=\n\n|\n\*\*)/s);
if (statusMatch) {
const statusText = statusMatch[1];
// "재진입 대기" 체크
if (statusText.includes('재진입 대기')) {
position = {
type: 'waiting',
cash: '$9,000'
};
}
// "손절 완료" 체크
else if (statusText.includes('손절 완료')) {
position = {
type: 'exited',
cash: '$9,000'
};
}
}
} catch (error) {
console.error(`⚠️ Failed to read MEMORY.md: ${error.message}`);
}
// 출력
console.log('\n📊 TQQQ 실시간 가격 (Polygon API)\n');
console.log('╭─────────────────┬───────────────────╮');
console.log('│ 항목 │ 값 │');
console.log('├─────────────────┼───────────────────┤');
console.log(`│ 현재가 (USD) │ $${price.toFixed(2).padStart(15)}`);
console.log(`│ 현재가 (KRW) │ ₩${krwPrice.toLocaleString('ko-KR').padStart(15)}`);
console.log(`│ 거래 시각 │ ${timestamp.toLocaleTimeString('ko-KR').padStart(15)}`);
console.log(`│ 거래량 │ ${data.s.toLocaleString('ko-KR').padStart(15)}`);
console.log(`│ 환율 │ $1 = ₩${CONFIG.USD_KRW.toLocaleString('ko-KR').padStart(10)}`);
console.log('╰─────────────────┴───────────────────╯');
if (position) {
if (position.type === 'waiting') {
console.log('\n💰 포지션: 재진입 대기 중');
console.log(` 현금: ${position.cash}`);
console.log(` 재진입 타이밍: 고용지표 발표 후 (22:30 KST)`);
} else if (position.type === 'exited') {
console.log('\n💰 포지션: 손절 완료');
console.log(` 현금: ${position.cash}`);
}
}
console.log('\n⚠ 데이터: Polygon 실시간 (지연 없음)');
}
// ============================================================================
// Main
// ============================================================================
async function main() {
console.log('🚀 TQQQ Polygon 모니터링\n');
// API 키 검증
if (!CONFIG.POLYGON_API_KEY) {
console.error('❌ POLYGON_API_KEY not found');
console.error(' Check: ~/.openclaw/openclaw.json → env.POLYGON_API_KEY');
process.exit(1);
}
try {
const data = await fetchWithRetry(CONFIG.SYMBOL, CONFIG.MAX_RETRIES);
formatOutput(data);
process.exit(0);
} catch (error) {
console.error('\n❌ Failed after all retries');
console.error(` Error: ${error.message}`);
process.exit(1);
}
}
// ============================================================================
// Run
// ============================================================================
if (require.main === module) {
main();
}
module.exports = { fetchPolygonQuote, fetchWithRetry, CONFIG };