b21d40333c
Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
5.1 KiB
TypeScript
144 lines
5.1 KiB
TypeScript
/**
|
|
* 매매 시그널 알림 — strategyId·전략명·종목별 live 설정으로 StrategyDto 해석
|
|
*/
|
|
import {
|
|
loadLiveStrategySettings,
|
|
loadStrategies,
|
|
loadStrategyForNotification,
|
|
type StrategyDto,
|
|
} from './backendApi';
|
|
import {
|
|
isStrategyMissingOnServer,
|
|
reconcileStrategyMissingWithValidIds,
|
|
unmarkStrategyMissingOnServer,
|
|
} from './strategyMissingCache';
|
|
import { coerceFiniteNumber } from './safeFormat';
|
|
import { hydrateStrategyDto, normalizeMarketCode } from './strategyHydrate';
|
|
import { extractVirtualConditions } from './virtualStrategyConditions';
|
|
|
|
let strategiesCache: StrategyDto[] | null = null;
|
|
let strategiesCacheAt = 0;
|
|
const CACHE_MS = 30_000;
|
|
|
|
export function normalizeStrategyId(id: unknown): number | null {
|
|
const n = coerceFiniteNumber(id);
|
|
return n != null && n > 0 ? Math.trunc(n) : null;
|
|
}
|
|
|
|
async function getStrategiesList(force = false): Promise<StrategyDto[]> {
|
|
const now = Date.now();
|
|
// 빈 배열은 캐시하지 않는다 — 백엔드 미준비로 빈 결과가 반환된 경우 재시도 허용
|
|
if (!force && strategiesCache && strategiesCache.length > 0 && now - strategiesCacheAt < CACHE_MS) {
|
|
return strategiesCache;
|
|
}
|
|
const list = await loadStrategies();
|
|
const result = list ?? [];
|
|
if (result.length > 0) {
|
|
strategiesCache = result;
|
|
strategiesCacheAt = now;
|
|
reconcileStrategyMissingWithValidIds(
|
|
result.map(s => s.id).filter((id): id is number => id != null && id > 0),
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function findStrategyByName(list: StrategyDto[], name: string): StrategyDto | undefined {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return undefined;
|
|
const exact = list.find(s => s.name?.trim() === trimmed);
|
|
if (exact) return exact;
|
|
const lower = trimmed.toLowerCase();
|
|
return list.find(s => {
|
|
const n = s.name?.trim().toLowerCase();
|
|
return n === lower || n?.includes(lower) || lower.includes(n ?? '');
|
|
});
|
|
}
|
|
|
|
function pickStrategy(dto: StrategyDto, strategyId: number): ResolvedNotificationStrategy {
|
|
const hydrated = hydrateStrategyDto(dto);
|
|
unmarkStrategyMissingOnServer(strategyId);
|
|
return { strategy: hydrated, strategyId };
|
|
}
|
|
|
|
function hasExtractableConditions(strategy: StrategyDto): boolean {
|
|
return extractVirtualConditions(hydrateStrategyDto(strategy)).length > 0;
|
|
}
|
|
|
|
export interface ResolvedNotificationStrategy {
|
|
strategy: StrategyDto | undefined;
|
|
/** live-conditions·차트에 사용할 유효 strategyId */
|
|
strategyId: number | null;
|
|
}
|
|
|
|
export async function resolveNotificationStrategy(
|
|
strategyId: unknown,
|
|
strategyName?: string | null,
|
|
prefetched?: StrategyDto[],
|
|
market?: string | null,
|
|
): Promise<ResolvedNotificationStrategy> {
|
|
const normalizedId = normalizeStrategyId(strategyId);
|
|
// prefetched가 빈 배열이면 직접 API로 재시도 (백엔드 미준비 시 prefetch가 빈 채로 캐시될 수 있음)
|
|
const list = (prefetched != null && prefetched.length > 0) ? prefetched : await getStrategiesList();
|
|
console.debug('[resolveStrategy] id=%s name=%s listSize=%d list=%s',
|
|
normalizedId, strategyName, list.length,
|
|
list.slice(0, 5).map(s => `#${s.id}:${s.name?.substring(0, 20)}`).join(' | '));
|
|
|
|
if (normalizedId != null) {
|
|
const fromList = list.find(s => s.id === normalizedId);
|
|
if (fromList?.id != null && hasExtractableConditions(fromList)) {
|
|
return pickStrategy(fromList, normalizedId);
|
|
}
|
|
if (!isStrategyMissingOnServer(normalizedId)) {
|
|
const loaded = await loadStrategyForNotification(normalizedId);
|
|
if (loaded && extractVirtualConditions(loaded).length > 0) {
|
|
return { strategy: loaded, strategyId: normalizedId };
|
|
}
|
|
if (loaded) {
|
|
return { strategy: loaded, strategyId: normalizedId };
|
|
}
|
|
}
|
|
}
|
|
|
|
const name = strategyName?.trim();
|
|
if (name) {
|
|
const byName = findStrategyByName(list, name);
|
|
if (byName?.id != null && byName.id > 0) {
|
|
if (hasExtractableConditions(byName)) {
|
|
return pickStrategy(byName, byName.id);
|
|
}
|
|
if (!isStrategyMissingOnServer(byName.id)) {
|
|
const loaded = await loadStrategyForNotification(byName.id);
|
|
if (loaded) {
|
|
return { strategy: loaded, strategyId: byName.id };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const normalizedMarket = market ? normalizeMarketCode(market) : null;
|
|
if (normalizedMarket) {
|
|
try {
|
|
const live = await loadLiveStrategySettings(normalizedMarket);
|
|
const liveId = normalizeStrategyId(live?.strategyId);
|
|
console.debug('[resolveStrategy] liveSettings %s → strategyId=%s', normalizedMarket, liveId);
|
|
if (liveId != null) {
|
|
const loaded = await loadStrategyForNotification(liveId);
|
|
if (loaded) {
|
|
return { strategy: loaded, strategyId: liveId };
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.debug('[resolveStrategy] liveSettings error:', e);
|
|
}
|
|
}
|
|
|
|
console.warn('[resolveStrategy] 전략 찾기 실패 id=%s name=%s market=%s listSize=%d', normalizedId, strategyName, market, list.length);
|
|
return { strategy: undefined, strategyId: normalizedId };
|
|
}
|
|
|
|
/** 알림 목록 진입 시 전략 목록 선로드 */
|
|
export async function prefetchNotificationStrategies(): Promise<StrategyDto[]> {
|
|
return getStrategiesList(true);
|
|
}
|