Files
goldenChart/frontend_golden/src/utils/alertMessageParser.ts
T
2026-05-23 15:11:48 +09:00

185 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 통합 알림 메시지 파싱
* 형식: "🎯 알림 조건 충족 (N개)\n종목명 × 전략명\n..."
* 한 알림에 여러 종목이 포함될 수 있음
*/
/**
* 메시지에서 조건 충족한 투자전략명 목록 추출
* "종목명 × 전략명" 형식의 각 라인에서 전략명 부분만 추출
*/
export function extractStrategyNames(message: string): string[] | null {
if (!message || !message.includes('알림 조건 충족') || !message.includes('개)')) return null;
const lines = message.split('\n').map((s) => s.trim()).filter(Boolean);
const strategyNames: string[] = [];
const SEPARATORS = [' × ', ' X '];
for (let i = 1; i < lines.length; i++) {
let idx = -1;
for (const sep of SEPARATORS) {
idx = lines[i].indexOf(sep);
if (idx >= 0) break;
}
if (idx >= 0) {
const name = lines[i].substring(idx + 3).trim();
if (name) strategyNames.push(name);
}
}
return strategyNames.length > 0 ? Array.from(new Set(strategyNames)) : null;
}
/**
* 메시지에서 조건 충족한 종목명(한글) 목록 추출
* "종목명 × 전략명" 형식의 각 라인에서 종목명 부분만 추출
*/
export function extractAssetNamesFromMessage(message: string): string[] | null {
if (!message || !message.includes('알림 조건 충족') || !message.includes('개)')) return null;
const lines = message.split('\n').map((s) => s.trim()).filter(Boolean);
const names: string[] = [];
const SEPARATORS = [' × ', ' X ']; // ×(유니코드), X(라틴) 모두 지원
for (let i = 1; i < lines.length; i++) {
let idx = -1;
for (const sep of SEPARATORS) {
idx = lines[i].indexOf(sep);
if (idx >= 0) break;
}
if (idx >= 0) {
const koreanName = lines[i].substring(0, idx).trim();
if (koreanName) names.push(koreanName);
}
}
return names.length > 0 ? Array.from(new Set(names)) : null;
}
/**
* 메시지에서 "종목명 × 전략명" 라인별 항목 추출 (종목+전략 쌍)
*/
export function extractStockStrategyPairs(message: string): Array<{ koreanName: string; strategyName: string }> | null {
if (!message || !message.includes('알림 조건 충족') || !message.includes('개)')) return null;
const lines = message.split('\n').map((s) => s.trim()).filter(Boolean);
const pairs: Array<{ koreanName: string; strategyName: string }> = [];
const SEPARATORS = [' × ', ' X '];
for (let i = 1; i < lines.length; i++) {
let idx = -1;
for (const sep of SEPARATORS) {
idx = lines[i].indexOf(sep);
if (idx >= 0) break;
}
if (idx >= 0) {
const koreanName = lines[i].substring(0, idx).trim();
const strategyName = lines[i].substring(idx + 3).trim();
if (koreanName) pairs.push({ koreanName, strategyName: strategyName || '' });
}
}
return pairs.length > 0 ? pairs : null;
}
/**
* 알림 목록에서 symbol → 전략명[] 매핑 추출
* 최신순 정렬된 알림에서, 각 종목별로 매칭된 전략명 수집 (중복 제거)
*/
export function extractSymbolToStrategyNames(
notifications: Array<{ message?: string; symbol?: string; koreanName?: string; triggeredAt?: string }>,
koreanToSymbol: Map<string, string>
): Record<string, string[]> {
const symbolToStrategies = new Map<string, Set<string>>();
const sorted = [...notifications].sort((a, b) => {
const at = a.triggeredAt ? new Date(a.triggeredAt).getTime() : 0;
const bt = b.triggeredAt ? new Date(b.triggeredAt).getTime() : 0;
return bt - at;
});
for (const n of sorted) {
const pairs = extractStockStrategyPairs(n.message || '');
if (pairs && pairs.length > 0) {
for (const { koreanName, strategyName } of pairs) {
if (!strategyName?.trim()) continue;
const row = resolveStockStrategyPairRow({ koreanName, strategyName }, koreanToSymbol);
const s = row.navSymbol;
if (s) {
if (!symbolToStrategies.has(s)) symbolToStrategies.set(s, new Set());
symbolToStrategies.get(s)!.add(strategyName.trim());
}
}
} else if (n.symbol && n.koreanName) {
// 단일 알림: 전략명은 메시지 첫 줄에서 추출
const firstLine = (n.message || '').split('\n')[0]?.trim() || '';
const strategyName = firstLine.replace(/^[\s🧪🎯✅]*/, '').trim();
if (strategyName) {
if (!symbolToStrategies.has(n.symbol)) symbolToStrategies.set(n.symbol, new Set());
symbolToStrategies.get(n.symbol)!.add(strategyName);
}
}
}
const result: Record<string, string[]> = {};
symbolToStrategies.forEach((strategies, symbol) => {
result[symbol] = Array.from(strategies);
});
return result;
}
/** 통합 알림 한 줄의 왼쪽이 마켓코드(KRW-BTC)인 경우 — cryptoMap 키는 한글명이라 resolveSymbols만으로는 매칭 안 됨 */
const UPBIT_MARKET_CODE_RE = /^(KRW|USDT|BTC)-[A-Z0-9]+$/i;
function koreanNameForMarketSymbol(symbol: string, cryptoMap: Map<string, string>, fallback: string): string {
const s = symbol.trim().toUpperCase();
const entries = Array.from(cryptoMap.entries());
for (let i = 0; i < entries.length; i++) {
const [koreanName, sym] = entries[i];
if (sym.toUpperCase() === s) return koreanName;
}
return fallback;
}
/**
* 통합 알림 종목 행 표시용: 한글명·심볼·업비트 URL용 navSymbol 정리
* - 메시지가 "KRW-BTC × CCI 1"처럼 마켓코드만 있으면 navSymbol을 채우고 한글 종목명으로 바꿉니다.
*/
export function resolveStockStrategyPairRow(
pair: { koreanName: string; strategyName: string; symbol?: string },
cryptoMap: Map<string, string>
): { koreanName: string; strategyName: string; symbol?: string; displaySymbol: string; navSymbol?: string } {
if (pair.symbol?.trim()) {
const navSymbol = pair.symbol.trim().toUpperCase();
const left = pair.koreanName?.trim() || '';
const koreanName =
left && !UPBIT_MARKET_CODE_RE.test(left) ? pair.koreanName : koreanNameForMarketSymbol(navSymbol, cryptoMap, left || navSymbol);
return { ...pair, koreanName, displaySymbol: navSymbol, navSymbol };
}
const raw = pair.koreanName?.trim() || '';
if (UPBIT_MARKET_CODE_RE.test(raw)) {
const navSymbol = raw.toUpperCase();
const koreanName = koreanNameForMarketSymbol(navSymbol, cryptoMap, raw);
return { ...pair, koreanName, displaySymbol: navSymbol, navSymbol };
}
const resolved = resolveSymbols([pair.koreanName], cryptoMap);
return {
...pair,
displaySymbol: resolved[0]?.symbol ?? pair.strategyName,
navSymbol: resolved[0]?.symbol,
};
}
/**
* koreanName → symbol 매핑 (getActiveCryptos 결과 활용)
* 공백 정규화 및 부분 일치로 한글명 표기 차이 보정
*/
export function resolveSymbols(
koreanNames: string[],
cryptoMap: Map<string, string>
): Array<{ symbol: string; koreanName: string }> {
const normalize = (s: string) => s.replace(/\s+/g, ' ').trim();
const entries = Array.from(cryptoMap.entries());
const result: Array<{ symbol: string; koreanName: string }> = [];
for (const kn of koreanNames) {
const n = normalize(kn);
let matched = entries.find(([k]) => normalize(k) === n || k === kn);
if (!matched) {
matched = entries.find(([k]) => normalize(k).includes(n) || n.includes(normalize(k)));
}
if (matched) result.push({ symbol: matched[1], koreanName: matched[0] });
}
return result;
}