AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
293
skills/openclaw-self-healing/scripts/tqqq-hybrid-monitor.js
Normal file
293
skills/openclaw-self-healing/scripts/tqqq-hybrid-monitor.js
Normal file
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TQQQ 하이브리드 모니터링 시스템
|
||||
*
|
||||
* 정규장 (09:30-16:00 EST): Finnhub WebSocket (실시간)
|
||||
* 확장 시간 (04:00-09:30, 16:00-20:00 EST): Polygon API (1분 폴링)
|
||||
*
|
||||
* Stop-Loss: $47.00
|
||||
*/
|
||||
|
||||
const WebSocket = require('ws');
|
||||
const https = require('https');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
// 환경변수 로드
|
||||
const FINNHUB_API_KEY = process.env.FINNHUB_API_KEY || '';
|
||||
const POLYGON_API_KEY = process.env.POLYGON_API_KEY || '';
|
||||
const STOP_LOSS_PRICE = parseFloat(process.env.TQQQ_STOP_LOSS || '47.00');
|
||||
const TICKER = 'TQQQ';
|
||||
|
||||
// Discord 알림 설정
|
||||
const DISCORD_CHANNEL = '1469190686145384513'; // #jarvis-market
|
||||
|
||||
// 상태 관리
|
||||
let lastPrice = null;
|
||||
let alertSent = false;
|
||||
let consecutiveBreaches = 0;
|
||||
const BREACH_THRESHOLD = 3; // 3회 연속 확인 후 알림
|
||||
|
||||
/**
|
||||
* 현재 시간이 정규장인지 확인 (EST 기준)
|
||||
*/
|
||||
function isMarketHours() {
|
||||
const now = new Date();
|
||||
const estOffset = -5 * 60; // EST = UTC-5
|
||||
const estTime = new Date(now.getTime() + (estOffset + now.getTimezoneOffset()) * 60000);
|
||||
|
||||
const day = estTime.getDay(); // 0=일, 6=토
|
||||
const hour = estTime.getHours();
|
||||
const minute = estTime.getMinutes();
|
||||
const totalMinutes = hour * 60 + minute;
|
||||
|
||||
// 주말 제외
|
||||
if (day === 0 || day === 6) return false;
|
||||
|
||||
// 정규장: 09:30 - 16:00 EST
|
||||
const marketOpen = 9 * 60 + 30; // 09:30
|
||||
const marketClose = 16 * 60; // 16:00
|
||||
|
||||
return totalMinutes >= marketOpen && totalMinutes < marketClose;
|
||||
}
|
||||
|
||||
/**
|
||||
* 확장 시간인지 확인 (프리마켓 + 애프터마켓)
|
||||
*/
|
||||
function isExtendedHours() {
|
||||
const now = new Date();
|
||||
const estOffset = -5 * 60;
|
||||
const estTime = new Date(now.getTime() + (estOffset + now.getTimezoneOffset()) * 60000);
|
||||
|
||||
const day = estTime.getDay();
|
||||
const hour = estTime.getHours();
|
||||
const minute = estTime.getMinutes();
|
||||
const totalMinutes = hour * 60 + minute;
|
||||
|
||||
// 주말 제외
|
||||
if (day === 0 || day === 6) return false;
|
||||
|
||||
// 프리마켓: 04:00 - 09:30 EST
|
||||
const premarketStart = 4 * 60;
|
||||
const premarketEnd = 9 * 60 + 30;
|
||||
|
||||
// 애프터마켓: 16:00 - 20:00 EST
|
||||
const aftermarketStart = 16 * 60;
|
||||
const aftermarketEnd = 20 * 60;
|
||||
|
||||
return (totalMinutes >= premarketStart && totalMinutes < premarketEnd) ||
|
||||
(totalMinutes >= aftermarketStart && totalMinutes < aftermarketEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop-Loss 체크 및 알림
|
||||
*/
|
||||
function checkStopLoss(price, source) {
|
||||
lastPrice = price;
|
||||
|
||||
if (price < STOP_LOSS_PRICE) {
|
||||
consecutiveBreaches++;
|
||||
console.log(`⚠️ [${source}] Price: $${price.toFixed(2)} | Stop-Loss: $${STOP_LOSS_PRICE} | Breaches: ${consecutiveBreaches}/${BREACH_THRESHOLD}`);
|
||||
|
||||
if (consecutiveBreaches >= BREACH_THRESHOLD && !alertSent) {
|
||||
sendDiscordAlert(price, source);
|
||||
alertSent = true;
|
||||
}
|
||||
} else {
|
||||
if (consecutiveBreaches > 0) {
|
||||
console.log(`✅ [${source}] Price recovered: $${price.toFixed(2)}`);
|
||||
}
|
||||
consecutiveBreaches = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord 알림 전송
|
||||
*/
|
||||
function sendDiscordAlert(price, source) {
|
||||
const message = `🚨 **TQQQ Stop-Loss 트리거**
|
||||
|
||||
**현재가:** $${price.toFixed(2)}
|
||||
**손절선:** $${STOP_LOSS_PRICE.toFixed(2)}
|
||||
**소스:** ${source}
|
||||
**시각:** ${new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })}
|
||||
|
||||
⚠️ 즉시 확인 필요!`;
|
||||
|
||||
const cmd = `message action:send channel:discord target:"${DISCORD_CHANNEL}" message:"${message.replace(/"/g, '\\"')}"`;
|
||||
|
||||
exec(`openclaw ${cmd}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`❌ Discord 알림 실패: ${error.message}`);
|
||||
} else {
|
||||
console.log('✅ Discord 알림 전송 완료');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finnhub WebSocket (정규장)
|
||||
*/
|
||||
function startFinnhubWebSocket() {
|
||||
console.log('🔌 Finnhub WebSocket 연결 시작...');
|
||||
|
||||
const ws = new WebSocket(`wss://ws.finnhub.io?token=${FINNHUB_API_KEY}`);
|
||||
|
||||
ws.on('open', () => {
|
||||
console.log('✅ Finnhub WebSocket 연결됨');
|
||||
ws.send(JSON.stringify({ type: 'subscribe', symbol: TICKER }));
|
||||
console.log(`📡 ${TICKER} 구독 시작`);
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
|
||||
if (message.type === 'trade' && message.data && message.data.length > 0) {
|
||||
const trades = message.data;
|
||||
trades.forEach(trade => {
|
||||
const price = trade.p;
|
||||
checkStopLoss(price, 'Finnhub WebSocket');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error(`❌ Finnhub WebSocket 에러: ${error.message}`);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('🔌 Finnhub WebSocket 연결 종료');
|
||||
// 정규장 시간이면 재연결
|
||||
if (isMarketHours()) {
|
||||
console.log('🔄 5초 후 재연결...');
|
||||
setTimeout(startFinnhubWebSocket, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polygon API 폴링 (확장 시간)
|
||||
*/
|
||||
function pollPolygonAPI() {
|
||||
const url = `https://api.polygon.io/v2/last/trade/${TICKER}?apiKey=${POLYGON_API_KEY}`;
|
||||
|
||||
https.get(url, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
|
||||
if (json.status === 'OK' && json.results && json.results.p) {
|
||||
const price = json.results.p;
|
||||
checkStopLoss(price, 'Polygon API');
|
||||
} else {
|
||||
console.error(`❌ Polygon API 에러: ${json.error || 'Unknown'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ JSON 파싱 에러: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}).on('error', (error) => {
|
||||
console.error(`❌ Polygon API 요청 실패: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 루프
|
||||
*/
|
||||
function main() {
|
||||
console.log('🚀 TQQQ 하이브리드 모니터링 시작');
|
||||
console.log(`📊 Ticker: ${TICKER}`);
|
||||
console.log(`🛑 Stop-Loss: $${STOP_LOSS_PRICE.toFixed(2)}`);
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
// API 키 검증
|
||||
if (!FINNHUB_API_KEY) {
|
||||
console.error('❌ FINNHUB_API_KEY 환경변수 필요');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!POLYGON_API_KEY) {
|
||||
console.error('❌ POLYGON_API_KEY 환경변수 필요');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let currentMode = null;
|
||||
let ws = null;
|
||||
let pollingInterval = null;
|
||||
|
||||
// 1분마다 모드 체크 및 전환
|
||||
setInterval(() => {
|
||||
const isMarket = isMarketHours();
|
||||
const isExtended = isExtendedHours();
|
||||
|
||||
if (isMarket && currentMode !== 'market') {
|
||||
console.log('\n🔔 정규장 시작 → Finnhub WebSocket 모드');
|
||||
currentMode = 'market';
|
||||
|
||||
// Polygon 폴링 중지
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
}
|
||||
|
||||
// Finnhub WebSocket 시작
|
||||
ws = startFinnhubWebSocket();
|
||||
|
||||
} else if (isExtended && currentMode !== 'extended') {
|
||||
console.log('\n🔔 확장 시간 시작 → Polygon 폴링 모드');
|
||||
currentMode = 'extended';
|
||||
|
||||
// Finnhub WebSocket 중지
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
|
||||
// Polygon 폴링 시작 (1분마다)
|
||||
pollingInterval = setInterval(pollPolygonAPI, 60000);
|
||||
pollPolygonAPI(); // 즉시 1회 실행
|
||||
|
||||
} else if (!isMarket && !isExtended && currentMode !== 'closed') {
|
||||
console.log('\n🔔 장 마감 → 대기 모드');
|
||||
currentMode = 'closed';
|
||||
|
||||
// 모든 모니터링 중지
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
}
|
||||
|
||||
// 알림 상태 리셋
|
||||
alertSent = false;
|
||||
consecutiveBreaches = 0;
|
||||
}
|
||||
}, 60000); // 1분마다 체크
|
||||
|
||||
// 초기 모드 설정
|
||||
if (isMarketHours()) {
|
||||
currentMode = 'market';
|
||||
ws = startFinnhubWebSocket();
|
||||
} else if (isExtendedHours()) {
|
||||
currentMode = 'extended';
|
||||
pollingInterval = setInterval(pollPolygonAPI, 60000);
|
||||
pollPolygonAPI();
|
||||
} else {
|
||||
currentMode = 'closed';
|
||||
console.log('⏸️ 현재 장 마감 시간 (대기 중)');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로그램 시작
|
||||
main();
|
||||
Reference in New Issue
Block a user