알림팝업 수정, 전략평가 로직 개선
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
resolveStrategyPrimaryTimeframe,
|
||||
} from '../../utils/strategyToChartIndicators';
|
||||
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
||||
import StrategyOscillatorPanes from './StrategyOscillatorPanes';
|
||||
|
||||
interface Props {
|
||||
symbol: string;
|
||||
@@ -154,10 +155,21 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
||||
return () => { cancelled = true; };
|
||||
}, [strategyId]);
|
||||
|
||||
/**
|
||||
* overlayVisibility 가 없는 경우(BacktestHistoryPage 컨텍스트)에는
|
||||
* 보조지표(오실레이터)를 TradingChart sub-pane 이 아닌 아래쪽
|
||||
* StrategyOscillatorPanes 섹션에서 표시한다.
|
||||
*/
|
||||
const showOscillatorPanel = !overlayVisibility;
|
||||
|
||||
const baseIndicators = useMemo(() => {
|
||||
let inds: IndicatorConfig[];
|
||||
if (strategy) {
|
||||
inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig);
|
||||
// 오실레이터 패널을 별도 섹션으로 표시할 때는 TradingChart 에서 제거
|
||||
if (showOscillatorPanel) {
|
||||
inds = inds.filter(i => isOverlayType(i.type));
|
||||
}
|
||||
inds = inds.filter(i => i.timeframeVisibility?.[chartTimeframe] !== false);
|
||||
} else {
|
||||
inds = defaultOverlayIndicators(getParams);
|
||||
@@ -166,7 +178,7 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
||||
inds = ensurePaperChartOverlays(inds, getParams, getVisualConfig);
|
||||
}
|
||||
return inds;
|
||||
}, [strategy, getParams, getVisualConfig, chartTimeframe, overlayVisibility]);
|
||||
}, [strategy, getParams, getVisualConfig, chartTimeframe, overlayVisibility, showOscillatorPanel]);
|
||||
|
||||
const indicators = useMemo(() => {
|
||||
let inds = overlayVisibility
|
||||
@@ -455,6 +467,9 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
||||
<div className="btd-analysis-error">표시할 캔들 데이터가 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
{showOscillatorPanel && !loading && bars.length > 0 && (
|
||||
<StrategyOscillatorPanes bars={bars} strategy={strategy} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -113,6 +113,16 @@ function saveHiddenIds(ids: Set<string>) {
|
||||
patchUiPreferences({ tradeNotifications: { hiddenIds: [...ids] } });
|
||||
}
|
||||
|
||||
/** 전체닫기·삭제 시각 (epoch 초) 로드 */
|
||||
function loadSuppressBefore(): number {
|
||||
return getUiPreferences().tradeNotifications?.suppressBefore ?? 0;
|
||||
}
|
||||
|
||||
/** 전체닫기·삭제 시각 저장 */
|
||||
function saveSuppressBefore(ts: number, immediate = false) {
|
||||
patchUiPreferences({ tradeNotifications: { suppressBefore: ts } }, immediate);
|
||||
}
|
||||
|
||||
export function useTradeNotification(): TradeNotificationContextValue {
|
||||
const ctx = useContext(TradeNotificationContext);
|
||||
if (!ctx) {
|
||||
@@ -157,6 +167,12 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
* 같은 틱에 수신된 신호 ID 까지 readIds 에 포함시킬 수 있다.
|
||||
*/
|
||||
const allSeenIdsRef = useRef<Set<string>>(new Set());
|
||||
/**
|
||||
* 전체닫기·삭제 실행 시각 (epoch 초).
|
||||
* candleTime < suppressBefore 인 오래된 신호는 팝업을 띄우지 않는다.
|
||||
* 백엔드 backfill 이 STOMP 로 수십 일치 과거 신호를 재전송해도 무시한다.
|
||||
*/
|
||||
const suppressBeforeRef = useRef<number>(loadSuppressBefore());
|
||||
const [detailSignal, setDetailSignal] = useState<TradeSignalInfo | null>(null);
|
||||
const [detailCentered, setDetailCentered] = useState(false);
|
||||
|
||||
@@ -176,7 +192,29 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
*/
|
||||
const cacheRead = loadReadIds();
|
||||
const memRead = readIdsRef.current;
|
||||
const read = new Set([...cacheRead, ...memRead]);
|
||||
const suppressBefore = suppressBeforeRef.current;
|
||||
|
||||
/**
|
||||
* suppressBefore 이전 캔들 신호: readIds 에 추가하고 isRead=true 처리.
|
||||
* DB 에 남아있는 오래된 신호가 폴링에서 미읽음으로 잡히는 것을 방지한다.
|
||||
*/
|
||||
const suppressed = dtos.filter(
|
||||
d => suppressBefore > 0 && (d.candleTime ?? 0) < suppressBefore,
|
||||
);
|
||||
let effectiveRead = new Set([...cacheRead, ...memRead]);
|
||||
if (suppressed.length > 0) {
|
||||
const suppressedIds = suppressed.map(
|
||||
d => `${d.market}:${d.candleTime}:${d.signalType}`,
|
||||
);
|
||||
const nextRead = new Set([...effectiveRead, ...suppressedIds]);
|
||||
if (nextRead.size !== effectiveRead.size) {
|
||||
readIdsRef.current = nextRead;
|
||||
effectiveRead = nextRead;
|
||||
saveReadIds(nextRead, true);
|
||||
}
|
||||
}
|
||||
|
||||
const read = effectiveRead;
|
||||
const hidden = opts?.rawServerList ? new Set<string>() : loadHiddenIds();
|
||||
const items = dtos
|
||||
.map(d => dtoToItem(d, read))
|
||||
@@ -252,6 +290,8 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
readIdsRef.current = merged;
|
||||
setReadIds(merged);
|
||||
}
|
||||
// 전체닫기·삭제 시각 복원 (재접속 후에도 오래된 신호 팝업 방지)
|
||||
suppressBeforeRef.current = loadSuppressBefore();
|
||||
setToastNotifications([]);
|
||||
void refreshHistory({ serverOnly: true, rawServerList: true });
|
||||
}, [refreshHistory, settingsLoaded, settingsSessionKey]);
|
||||
@@ -276,19 +316,29 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
// 수신한 모든 ID 를 동기적으로 누적 (dismissAllToasts 가 참조)
|
||||
allSeenIdsRef.current.add(id);
|
||||
|
||||
/**
|
||||
* ── 오래된 신호 억제 (핵심 필터) ───────────────────────────────────────
|
||||
* 전체닫기·삭제 시각(suppressBefore) 이전에 시작된 캔들의 신호는
|
||||
* 백엔드 backfill 이 STOMP 로 재전송해도 팝업을 띄우지 않는다.
|
||||
* candleTime 은 epoch 초 단위이고 suppressBefore 도 epoch 초이다.
|
||||
*/
|
||||
const isSuppressed =
|
||||
suppressBeforeRef.current > 0 &&
|
||||
signal.candleTime < suppressBeforeRef.current;
|
||||
|
||||
/**
|
||||
* 읽음 여부 3중 확인:
|
||||
* 1. readIdsRef.current — dismissAllToasts 가 즉시 업데이트하는 인메모리 ref
|
||||
* 2. loadReadIds() — DB 캐시 기반 값
|
||||
* 3. allNotificationsRef — dismissAllToasts 의 functional updater 가
|
||||
* isRead:true 를 예약했지만 readIdsRef 에 아직 반영되지 않은 경우 커버
|
||||
* 3. allNotificationsRef — functional updater 가 isRead:true 예약 후 ref 미반영 커버
|
||||
*/
|
||||
const alreadyRead =
|
||||
isSuppressed ||
|
||||
readIdsRef.current.has(id) ||
|
||||
loadReadIds().has(id) ||
|
||||
(allNotificationsRef.current.find(n => n.id === id)?.isRead === true);
|
||||
|
||||
// readIds 에 누락된 읽음 항목 보정 (다음 STOMP/폴링에서 즉시 필터링)
|
||||
// readIds 에 누락된 읽음 항목 보정
|
||||
if (alreadyRead && !readIdsRef.current.has(id)) {
|
||||
const next = new Set(readIdsRef.current);
|
||||
next.add(id);
|
||||
@@ -389,6 +439,11 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
...allSeenIdsRef.current, // 동일 틱에 수신됐지만 ref 미반영 ID
|
||||
]);
|
||||
|
||||
// 전체닫기 시각 기록 — 이 시각 이전 candleTime 신호는 이후 팝업 금지
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
suppressBeforeRef.current = nowSec;
|
||||
saveSuppressBefore(nowSec, true);
|
||||
|
||||
// 1) 인메모리 ref 즉시 갱신 (다음 addNotification 호출에서 즉시 참조)
|
||||
readIdsRef.current = nextRead;
|
||||
|
||||
@@ -490,18 +545,21 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
]),
|
||||
];
|
||||
|
||||
// 전체 삭제 시각 기록 — 이 시각 이전 candleTime 신호는 이후 팝업 금지
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
suppressBeforeRef.current = nowSec;
|
||||
saveSuppressBefore(nowSec, true);
|
||||
|
||||
/**
|
||||
* readIds 를 new Set() 으로 초기화하지 않는다.
|
||||
* 초기화하면 백엔드가 삭제된 신호를 STOMP 로 재전송할 때
|
||||
* readIds 에 없어서 팝업이 다시 뜨는 버그가 발생한다.
|
||||
* 대신 삭제된 ID 를 readIds 에 추가하여 재전송 신호를 필터링한다.
|
||||
* readIds 에 삭제된 ID 를 추가 (초기화 X).
|
||||
* 백엔드가 동일 신호를 STOMP 로 재전송해도 suppressBefore + readIds 이중 필터.
|
||||
*/
|
||||
const nextRead = new Set([...readIdsRef.current, ...ids]);
|
||||
readIdsRef.current = nextRead;
|
||||
setReadIds(nextRead);
|
||||
saveReadIds(nextRead, true);
|
||||
|
||||
// hiddenIds 는 초기화 (전체 삭제 후 목록 완전히 비움)
|
||||
// hiddenIds 초기화 (전체 삭제 후 목록 완전히 비움)
|
||||
saveHiddenIds(new Set());
|
||||
|
||||
setToastNotifications([]);
|
||||
|
||||
@@ -33,6 +33,12 @@ export interface UiPreferences {
|
||||
tradeNotifications?: {
|
||||
readIds?: string[];
|
||||
hiddenIds?: string[];
|
||||
/**
|
||||
* 전체닫기·전체삭제 실행 시각 (epoch 초).
|
||||
* candleTime < suppressBefore 인 신호는 팝업 표시하지 않는다.
|
||||
* 백엔드 backfill 로 오래된 신호가 STOMP 재전송되어도 팝업이 뜨지 않게 한다.
|
||||
*/
|
||||
suppressBefore?: number;
|
||||
/** 매매 시그널 알림 목록 화면 — list | grid */
|
||||
listLayout?: 'list' | 'grid';
|
||||
/** 그리드형 열 배치 프리셋 id (tradeNotificationGridPresets) */
|
||||
|
||||
Reference in New Issue
Block a user