알림목록 전략지표 없음 문제 수정
This commit is contained in:
@@ -148,12 +148,13 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
item.strategyId,
|
item.strategyId,
|
||||||
item.strategyName,
|
item.strategyName,
|
||||||
prefetchedStrategies,
|
prefetchedStrategies,
|
||||||
|
item.market,
|
||||||
).then(({ strategy: s, strategyId: sid }) => {
|
).then(({ strategy: s, strategyId: sid }) => {
|
||||||
if (!cancelled && s) setStrategy(s);
|
if (!cancelled && s) setStrategy(s);
|
||||||
else if (!cancelled && !s && sid == null) setStrategy(undefined);
|
else if (!cancelled && !s && sid == null) setStrategy(undefined);
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [item.strategyId, item.strategyName, panelActive, isListLayout, prefetchedStrategies]);
|
}, [item.strategyId, item.strategyName, item.market, panelActive, isListLayout, prefetchedStrategies]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExpandedChartKey(null);
|
setExpandedChartKey(null);
|
||||||
|
|||||||
@@ -9,22 +9,24 @@ import {
|
|||||||
resolveNotificationStrategy,
|
resolveNotificationStrategy,
|
||||||
normalizeStrategyId,
|
normalizeStrategyId,
|
||||||
} from '../utils/resolveNotificationStrategy';
|
} from '../utils/resolveNotificationStrategy';
|
||||||
|
import { hydrateStrategyDto, normalizeMarketCode } from '../utils/strategyHydrate';
|
||||||
import {
|
import {
|
||||||
fetchLiveSnapshot,
|
fetchLiveSnapshot,
|
||||||
type VirtualIndicatorSnapshot,
|
type VirtualIndicatorSnapshot,
|
||||||
} from './useVirtualIndicatorSnapshots';
|
} from './useVirtualIndicatorSnapshots';
|
||||||
|
|
||||||
const MAX_EMPTY_RETRIES = 10;
|
const MAX_EMPTY_RETRIES = 15;
|
||||||
|
|
||||||
function staticFallback(
|
function staticFallback(
|
||||||
market: string,
|
market: string,
|
||||||
strategyId: number,
|
strategyId: number,
|
||||||
strategy: StrategyDto,
|
strategy: StrategyDto,
|
||||||
): VirtualIndicatorSnapshot | undefined {
|
): VirtualIndicatorSnapshot | undefined {
|
||||||
const baseRows = extractVirtualConditions(strategy);
|
const hydrated = hydrateStrategyDto(strategy);
|
||||||
|
const baseRows = extractVirtualConditions(hydrated);
|
||||||
if (baseRows.length === 0) return undefined;
|
if (baseRows.length === 0) return undefined;
|
||||||
return {
|
return {
|
||||||
market,
|
market: normalizeMarketCode(market),
|
||||||
strategyId,
|
strategyId,
|
||||||
timeframe: [...new Set(baseRows.map(r => r.timeframe))].join(', '),
|
timeframe: [...new Set(baseRows.map(r => r.timeframe))].join(', '),
|
||||||
rows: baseRows.map(r => normalizeConditionRow({ ...r, currentValue: null, satisfied: null })),
|
rows: baseRows.map(r => normalizeConditionRow({ ...r, currentValue: null, satisfied: null })),
|
||||||
@@ -49,6 +51,7 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
const refreshInFlightRef = useRef(false);
|
const refreshInFlightRef = useRef(false);
|
||||||
const resolvedStrategyRef = useRef<StrategyDto | undefined>();
|
const resolvedStrategyRef = useRef<StrategyDto | undefined>();
|
||||||
const effectiveStrategyIdRef = useRef<number | null>(null);
|
const effectiveStrategyIdRef = useRef<number | null>(null);
|
||||||
|
const normalizedMarket = normalizeMarketCode(market);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
const rawId = normalizeStrategyId(strategyId);
|
const rawId = normalizeStrategyId(strategyId);
|
||||||
@@ -65,14 +68,19 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let effectiveStrategy = strategy ?? resolvedStrategyRef.current;
|
let effectiveStrategy = strategy ? hydrateStrategyDto(strategy) : resolvedStrategyRef.current;
|
||||||
let effectiveId = effectiveStrategy?.id ?? effectiveStrategyIdRef.current ?? rawId;
|
let effectiveId = effectiveStrategy?.id ?? effectiveStrategyIdRef.current ?? rawId;
|
||||||
|
|
||||||
if (!effectiveStrategy || effectiveStrategy.id !== rawId) {
|
const needsResolve = !effectiveStrategy
|
||||||
|
|| extractVirtualConditions(effectiveStrategy).length === 0
|
||||||
|
|| effectiveStrategy.id !== rawId;
|
||||||
|
|
||||||
|
if (needsResolve) {
|
||||||
const resolved = await resolveNotificationStrategy(
|
const resolved = await resolveNotificationStrategy(
|
||||||
rawId,
|
rawId,
|
||||||
strategyName,
|
strategyName,
|
||||||
prefetchedStrategies,
|
prefetchedStrategies,
|
||||||
|
normalizedMarket,
|
||||||
);
|
);
|
||||||
if (gen !== genRef.current) return;
|
if (gen !== genRef.current) return;
|
||||||
if (resolved.strategy) {
|
if (resolved.strategy) {
|
||||||
@@ -86,11 +94,17 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pinFirst = emptyRetryRef.current === 0;
|
const pinFirst = emptyRetryRef.current === 0;
|
||||||
let snap = await fetchLiveSnapshot(market, effectiveId, effectiveStrategy, pinFirst);
|
let snap = await fetchLiveSnapshot(
|
||||||
|
normalizedMarket,
|
||||||
|
effectiveId,
|
||||||
|
effectiveStrategy,
|
||||||
|
pinFirst,
|
||||||
|
{ readOnlyPin: true },
|
||||||
|
);
|
||||||
if (gen !== genRef.current) return;
|
if (gen !== genRef.current) return;
|
||||||
|
|
||||||
if (!snap && effectiveStrategy) {
|
if ((!snap || snap.rows.length === 0) && effectiveStrategy) {
|
||||||
snap = staticFallback(market, effectiveId, effectiveStrategy) ?? null;
|
snap = staticFallback(normalizedMarket, effectiveId, effectiveStrategy) ?? snap;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snap && snap.rows.length > 0) {
|
if (snap && snap.rows.length > 0) {
|
||||||
@@ -108,7 +122,15 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
} finally {
|
} finally {
|
||||||
refreshInFlightRef.current = false;
|
refreshInFlightRef.current = false;
|
||||||
}
|
}
|
||||||
}, [market, strategyId, strategy, strategyName, enabled, prefetchedStrategies]);
|
}, [
|
||||||
|
market,
|
||||||
|
normalizedMarket,
|
||||||
|
strategyId,
|
||||||
|
strategy,
|
||||||
|
strategyName,
|
||||||
|
enabled,
|
||||||
|
prefetchedStrategies,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const rawId = normalizeStrategyId(strategyId);
|
const rawId = normalizeStrategyId(strategyId);
|
||||||
@@ -121,7 +143,7 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emptyRetryRef.current = 0;
|
emptyRetryRef.current = 0;
|
||||||
resolvedStrategyRef.current = strategy;
|
resolvedStrategyRef.current = strategy ? hydrateStrategyDto(strategy) : undefined;
|
||||||
effectiveStrategyIdRef.current = strategy?.id ?? rawId;
|
effectiveStrategyIdRef.current = strategy?.id ?? rawId;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
void refresh();
|
void refresh();
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import {
|
|||||||
pinCandleWatch,
|
pinCandleWatch,
|
||||||
type LiveConditionRowDto,
|
type LiveConditionRowDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
import { pinStrategyEvaluationTimeframes, pinReadOnlyStrategyTimeframes } from '../utils/strategyTimeframePin';
|
||||||
import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry';
|
import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry';
|
||||||
import { coerceFiniteNumber } from '../utils/safeFormat';
|
import { coerceFiniteNumber } from '../utils/safeFormat';
|
||||||
import { normalizeStrategyId } from '../utils/resolveNotificationStrategy';
|
import { normalizeStrategyId } from '../utils/resolveNotificationStrategy';
|
||||||
|
import { hydrateStrategyDto, normalizeMarketCode } from '../utils/strategyHydrate';
|
||||||
import {
|
import {
|
||||||
extractVirtualConditions,
|
extractVirtualConditions,
|
||||||
type VirtualConditionRow,
|
type VirtualConditionRow,
|
||||||
@@ -113,51 +114,67 @@ function staticSnapshot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusMatches(status: { market?: string; strategyId?: unknown }, market: string, strategyId: number): boolean {
|
function statusMatches(status: { market?: string; strategyId?: unknown }, market: string, strategyId: number): boolean {
|
||||||
if (!status?.market || status.market !== market) return false;
|
if (!status?.market) return false;
|
||||||
|
if (normalizeMarketCode(status.market) !== normalizeMarketCode(market)) return false;
|
||||||
const sid = normalizeStrategyId(status.strategyId);
|
const sid = normalizeStrategyId(status.strategyId);
|
||||||
const expected = normalizeStrategyId(strategyId);
|
const expected = normalizeStrategyId(strategyId);
|
||||||
return sid != null && expected != null && sid === expected;
|
return sid != null && expected != null && sid === expected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FetchLiveSnapshotOptions {
|
||||||
|
/** true — syncStrategyTimeframes·repair 생략 (알림 목록) */
|
||||||
|
readOnlyPin?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */
|
/** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */
|
||||||
export async function fetchLiveSnapshot(
|
export async function fetchLiveSnapshot(
|
||||||
market: string,
|
market: string,
|
||||||
strategyId: number,
|
strategyId: number,
|
||||||
strategy: StrategyDto | undefined,
|
strategy: StrategyDto | undefined,
|
||||||
pinFirst: boolean,
|
pinFirst: boolean,
|
||||||
|
options?: FetchLiveSnapshotOptions,
|
||||||
): Promise<VirtualIndicatorSnapshot | null> {
|
): Promise<VirtualIndicatorSnapshot | null> {
|
||||||
const baseRows = strategy ? extractVirtualConditions(strategy) : [];
|
const normalizedMarket = normalizeMarketCode(market);
|
||||||
|
const hydratedStrategy = strategy ? hydrateStrategyDto(strategy) : undefined;
|
||||||
|
const baseRows = hydratedStrategy ? extractVirtualConditions(hydratedStrategy) : [];
|
||||||
|
|
||||||
if (pinFirst) {
|
if (pinFirst) {
|
||||||
try {
|
try {
|
||||||
await pinStrategyEvaluationTimeframes(market, strategyId);
|
if (options?.readOnlyPin) {
|
||||||
|
await pinReadOnlyStrategyTimeframes(normalizedMarket, strategyId, hydratedStrategy);
|
||||||
|
} else {
|
||||||
|
await pinStrategyEvaluationTimeframes(normalizedMarket, strategyId);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
await pinCandleWatch(market, '1m');
|
await pinCandleWatch(normalizedMarket, '3m');
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
try {
|
||||||
|
await pinCandleWatch(normalizedMarket, '1m');
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let status: Awaited<ReturnType<typeof fetchLiveConditionStatus>> = null;
|
let status: Awaited<ReturnType<typeof fetchLiveConditionStatus>> = null;
|
||||||
try {
|
try {
|
||||||
status = await fetchLiveConditionStatus(market, strategyId);
|
status = await fetchLiveConditionStatus(normalizedMarket, strategyId);
|
||||||
} catch {
|
} catch {
|
||||||
status = null;
|
status = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!status || !statusMatches(status, market, strategyId)) {
|
if (!status || !statusMatches(status, normalizedMarket, strategyId)) {
|
||||||
if (baseRows.length === 0) return null;
|
if (baseRows.length === 0) return null;
|
||||||
return staticSnapshot(market, strategyId, baseRows);
|
return staticSnapshot(normalizedMarket, strategyId, baseRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = mergeRows(baseRows, status.rows ?? []);
|
const rows = mergeRows(baseRows, status.rows ?? []);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
if (baseRows.length === 0) return null;
|
if (baseRows.length === 0) return null;
|
||||||
return staticSnapshot(market, strategyId, baseRows);
|
return staticSnapshot(normalizedMarket, strategyId, baseRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
market,
|
market: normalizedMarket,
|
||||||
strategyId,
|
strategyId,
|
||||||
timeframe: status.timeframe || [...new Set(baseRows.map(r => r.timeframe))].join(', '),
|
timeframe: status.timeframe || [...new Set(baseRows.map(r => r.timeframe))].join(', '),
|
||||||
rows,
|
rows,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
|
|||||||
import {
|
import {
|
||||||
isStrategyMissingOnServer,
|
isStrategyMissingOnServer,
|
||||||
markStrategyMissingFromPath,
|
markStrategyMissingFromPath,
|
||||||
|
unmarkStrategyMissingOnServer,
|
||||||
} from './strategyMissingCache';
|
} from './strategyMissingCache';
|
||||||
|
|
||||||
export { isStrategyMissingOnServer } from './strategyMissingCache';
|
export { isStrategyMissingOnServer } from './strategyMissingCache';
|
||||||
@@ -1097,6 +1098,14 @@ export async function loadStrategy(id: number): Promise<StrategyDto | null> {
|
|||||||
return request<StrategyDto>(`/strategies/${id}`);
|
return request<StrategyDto>(`/strategies/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 알림·지표 표시 — missing 캐시 무시, 성공 시 캐시 해제 */
|
||||||
|
export async function loadStrategyForNotification(id: number): Promise<StrategyDto | null> {
|
||||||
|
if (!hasRegisteredUser() || id <= 0) return null;
|
||||||
|
const dto = await request<StrategyDto>(`/strategies/${id}`);
|
||||||
|
if (dto) unmarkStrategyMissingOnServer(id);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
/** 전략 저장 (id 없으면 신규, 있으면 수정) — 실패 시 Error throw */
|
/** 전략 저장 (id 없으면 신규, 있으면 수정) — 실패 시 Error throw */
|
||||||
export async function saveStrategy(dto: StrategyDto): Promise<StrategyDto> {
|
export async function saveStrategy(dto: StrategyDto): Promise<StrategyDto> {
|
||||||
const res = await fetch(`${API_BASE}/strategies`, {
|
const res = await fetch(`${API_BASE}/strategies`, {
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* 매매 시그널 알림 — strategyId·전략명으로 StrategyDto 해석
|
* 매매 시그널 알림 — strategyId·전략명·종목별 live 설정으로 StrategyDto 해석
|
||||||
* (서버 DB와 알림 이력의 strategyId 불일치 시 전략명 fallback)
|
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
isStrategyMissingOnServer,
|
loadLiveStrategySettings,
|
||||||
loadStrategies,
|
loadStrategies,
|
||||||
loadStrategy,
|
loadStrategyForNotification,
|
||||||
type StrategyDto,
|
type StrategyDto,
|
||||||
} from './backendApi';
|
} from './backendApi';
|
||||||
import { reconcileStrategyMissingWithValidIds } from './strategyMissingCache';
|
import { reconcileStrategyMissingWithValidIds, unmarkStrategyMissingOnServer } from './strategyMissingCache';
|
||||||
import { coerceFiniteNumber } from './safeFormat';
|
import { coerceFiniteNumber } from './safeFormat';
|
||||||
|
import { hydrateStrategyDto, normalizeMarketCode } from './strategyHydrate';
|
||||||
|
import { extractVirtualConditions } from './virtualStrategyConditions';
|
||||||
|
|
||||||
let strategiesCache: StrategyDto[] | null = null;
|
let strategiesCache: StrategyDto[] | null = null;
|
||||||
let strategiesCacheAt = 0;
|
let strategiesCacheAt = 0;
|
||||||
@@ -34,6 +35,28 @@ async function getStrategiesList(force = false): Promise<StrategyDto[]> {
|
|||||||
return strategiesCache;
|
return strategiesCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export interface ResolvedNotificationStrategy {
|
||||||
strategy: StrategyDto | undefined;
|
strategy: StrategyDto | undefined;
|
||||||
/** live-conditions·차트에 사용할 유효 strategyId */
|
/** live-conditions·차트에 사용할 유효 strategyId */
|
||||||
@@ -44,16 +67,20 @@ export async function resolveNotificationStrategy(
|
|||||||
strategyId: unknown,
|
strategyId: unknown,
|
||||||
strategyName?: string | null,
|
strategyName?: string | null,
|
||||||
prefetched?: StrategyDto[],
|
prefetched?: StrategyDto[],
|
||||||
|
market?: string | null,
|
||||||
): Promise<ResolvedNotificationStrategy> {
|
): Promise<ResolvedNotificationStrategy> {
|
||||||
const normalizedId = normalizeStrategyId(strategyId);
|
const normalizedId = normalizeStrategyId(strategyId);
|
||||||
const list = prefetched ?? await getStrategiesList();
|
const list = prefetched ?? await getStrategiesList();
|
||||||
|
|
||||||
if (normalizedId != null && !isStrategyMissingOnServer(normalizedId)) {
|
if (normalizedId != null) {
|
||||||
const fromList = list.find(s => s.id === normalizedId);
|
const fromList = list.find(s => s.id === normalizedId);
|
||||||
if (fromList) {
|
if (fromList?.id != null && hasExtractableConditions(fromList)) {
|
||||||
return { strategy: fromList, strategyId: normalizedId };
|
return pickStrategy(fromList, normalizedId);
|
||||||
|
}
|
||||||
|
const loaded = await loadStrategyForNotification(normalizedId);
|
||||||
|
if (loaded && extractVirtualConditions(loaded).length > 0) {
|
||||||
|
return { strategy: loaded, strategyId: normalizedId };
|
||||||
}
|
}
|
||||||
const loaded = await loadStrategy(normalizedId);
|
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
return { strategy: loaded, strategyId: normalizedId };
|
return { strategy: loaded, strategyId: normalizedId };
|
||||||
}
|
}
|
||||||
@@ -61,9 +88,31 @@ export async function resolveNotificationStrategy(
|
|||||||
|
|
||||||
const name = strategyName?.trim();
|
const name = strategyName?.trim();
|
||||||
if (name) {
|
if (name) {
|
||||||
const byName = list.find(s => s.name?.trim() === name);
|
const byName = findStrategyByName(list, name);
|
||||||
if (byName?.id != null && byName.id > 0) {
|
if (byName?.id != null && byName.id > 0) {
|
||||||
return { strategy: byName, strategyId: byName.id };
|
if (hasExtractableConditions(byName)) {
|
||||||
|
return pickStrategy(byName, 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);
|
||||||
|
if (liveId != null) {
|
||||||
|
const loaded = await loadStrategyForNotification(liveId);
|
||||||
|
if (loaded) {
|
||||||
|
return { strategy: loaded, strategyId: liveId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 전략 DTO 정규화 — API·DB에서 문자열 JSON·flowLayout 보정
|
||||||
|
*/
|
||||||
|
import type { LogicNode } from './strategyTypes';
|
||||||
|
import type { StrategyDto } from './backendApi';
|
||||||
|
import { extractVirtualConditions } from './virtualStrategyConditions';
|
||||||
|
import { decodeConditionForEditor, encodeConditionForSave } from './strategyConditionSerde';
|
||||||
|
import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
|
||||||
|
|
||||||
|
export function asLogicNode(raw: unknown): LogicNode | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
const t = raw.trim();
|
||||||
|
if (!t) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(t) as LogicNode;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof raw === 'object') return raw as LogicNode;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeMarketCode(market: string): string {
|
||||||
|
const s = market.trim().toUpperCase();
|
||||||
|
if (!s) return 'KRW-BTC';
|
||||||
|
return s.startsWith('KRW-') ? s : `KRW-${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** buy/sell DSL 파싱·flowLayout 재인코딩으로 조건 추출 가능 상태로 보정 */
|
||||||
|
export function hydrateStrategyDto(strategy: StrategyDto): StrategyDto {
|
||||||
|
const buy = asLogicNode(strategy.buyCondition);
|
||||||
|
const sell = asLogicNode(strategy.sellCondition);
|
||||||
|
let next: StrategyDto = {
|
||||||
|
...strategy,
|
||||||
|
buyCondition: buy ?? undefined,
|
||||||
|
sellCondition: sell ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (extractVirtualConditions(next).length > 0) return next;
|
||||||
|
|
||||||
|
const layout = strategy.flowLayout as StrategyFlowLayoutStore | null | undefined;
|
||||||
|
if (!layout) return next;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buyState = decodeConditionForEditor(buy);
|
||||||
|
const sellState = decodeConditionForEditor(sell);
|
||||||
|
const encodedBuy = encodeConditionForSave(buyState);
|
||||||
|
const encodedSell = encodeConditionForSave(sellState);
|
||||||
|
if (encodedBuy || encodedSell) {
|
||||||
|
next = {
|
||||||
|
...next,
|
||||||
|
buyCondition: encodedBuy ?? buy ?? undefined,
|
||||||
|
sellCondition: encodedSell ?? sell ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* flowLayout 보정 실패 — 원본 유지 */
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
@@ -42,6 +42,11 @@ export function markStrategyMissingOnServer(strategyId: number): void {
|
|||||||
persistStrategyMissingCache();
|
persistStrategyMissingCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unmarkStrategyMissingOnServer(strategyId: number): void {
|
||||||
|
if (strategyId <= 0) return;
|
||||||
|
if (strategyMissingIds.delete(strategyId)) persistStrategyMissingCache();
|
||||||
|
}
|
||||||
|
|
||||||
/** 전략 목록에 다시 나타난 ID 는 missing 에서 제거 */
|
/** 전략 목록에 다시 나타난 ID 는 missing 에서 제거 */
|
||||||
export function reconcileStrategyMissingWithValidIds(validIds: Iterable<number>): void {
|
export function reconcileStrategyMissingWithValidIds(validIds: Iterable<number>): void {
|
||||||
const valid = new Set(validIds);
|
const valid = new Set(validIds);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { loadStrategyTimeframes, pinCandleWatch, repairStrategyTimeframes } from './backendApi';
|
import { loadStrategyTimeframes, pinCandleWatch, repairStrategyTimeframes, type StrategyDto } from './backendApi';
|
||||||
import { normalizeStartCandleType } from './strategyStartNodes';
|
import { normalizeStartCandleType } from './strategyStartNodes';
|
||||||
import { syncStrategyTimeframesFromLayoutIfNeeded } from './strategyTimeframeSync';
|
import { collectUiEvaluationTimeframes, syncStrategyTimeframesFromLayoutIfNeeded } from './strategyTimeframeSync';
|
||||||
|
|
||||||
/** 전략 DSL 평가 분봉 전체 pin — 1m 집계·상위 봉 warm-up */
|
/** 전략 DSL 평가 분봉 전체 pin — 1m 집계·상위 봉 warm-up */
|
||||||
export async function pinStrategyEvaluationTimeframes(
|
export async function pinStrategyEvaluationTimeframes(
|
||||||
@@ -22,3 +22,30 @@ export async function pinStrategyEvaluationTimeframes(
|
|||||||
}
|
}
|
||||||
return timeframes;
|
return timeframes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 알림 목록 — DB sync/repair 없이 평가 분봉만 pin (서버 read-only 안전) */
|
||||||
|
export async function pinReadOnlyStrategyTimeframes(
|
||||||
|
market: string,
|
||||||
|
strategyId: number,
|
||||||
|
strategy?: StrategyDto,
|
||||||
|
): Promise<void> {
|
||||||
|
let timeframes: string[] = [];
|
||||||
|
try {
|
||||||
|
timeframes = collectUiEvaluationTimeframes(strategyId, strategy);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
if (timeframes.length === 0) {
|
||||||
|
try {
|
||||||
|
const raw = await loadStrategyTimeframes(strategyId);
|
||||||
|
timeframes = raw.length ? raw : ['1m'];
|
||||||
|
} catch {
|
||||||
|
timeframes = ['1m'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unique = [...new Set(timeframes.map(tf => normalizeStartCandleType(tf)))];
|
||||||
|
if (!unique.includes('1m')) unique.unshift('1m');
|
||||||
|
for (const tf of unique) {
|
||||||
|
await pinCandleWatch(market, tf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { CONDITION_LABEL } from './strategyTypes';
|
|||||||
import type { StrategyDto } from './backendApi';
|
import type { StrategyDto } from './backendApi';
|
||||||
import { formatIndicatorDisplayLabel } from './indicatorRegistry';
|
import { formatIndicatorDisplayLabel } from './indicatorRegistry';
|
||||||
import { coerceFiniteNumber, safeToFixed } from './safeFormat';
|
import { coerceFiniteNumber, safeToFixed } from './safeFormat';
|
||||||
|
import { asLogicNode } from './strategyHydrate';
|
||||||
|
|
||||||
export { coerceFiniteNumber } from './safeFormat';
|
export { coerceFiniteNumber } from './safeFormat';
|
||||||
|
|
||||||
@@ -79,8 +80,8 @@ export function extractVirtualConditions(strategy: StrategyDto | null | undefine
|
|||||||
if (!strategy) return [];
|
if (!strategy) return [];
|
||||||
const out: VirtualConditionRow[] = [];
|
const out: VirtualConditionRow[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
walk(strategy.buyCondition as LogicNode | null, '1m', 'buy', out, seen);
|
walk(asLogicNode(strategy.buyCondition), '1m', 'buy', out, seen);
|
||||||
walk(strategy.sellCondition as LogicNode | null, '1m', 'sell', out, seen);
|
walk(asLogicNode(strategy.sellCondition), '1m', 'sell', out, seen);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user