goldenChat base source add
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user