AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
199
skills/openclaw-self-healing/scripts/tqqq-polygon-monitor.js
Normal file
199
skills/openclaw-self-healing/scripts/tqqq-polygon-monitor.js
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/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 };
|
||||
Reference in New Issue
Block a user