goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,184 @@
/**
* 통합 알림 메시지 파싱
* 형식: "🎯 알림 조건 충족 (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;
}