전략분봉 저장 에러 문제 수정
This commit is contained in:
+152
-17
@@ -78,7 +78,9 @@ import {
|
||||
DEFAULT_BACKTEST_SETTINGS,
|
||||
loadBacktestSettings,
|
||||
loadStrategies as loadStrategiesForLive,
|
||||
loadLiveStrategySettings,
|
||||
type LiveStrategySettingsDto,
|
||||
type BacktestSignal,
|
||||
} from './utils/backendApi';
|
||||
import { useIsMobile } from './hooks/useMediaQuery';
|
||||
import MobileChartDock from './components/MobileChartDock';
|
||||
@@ -94,6 +96,8 @@ import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
||||
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
||||
import './App.css';
|
||||
import './styles/appPopup.css';
|
||||
import './styles/paperDashboard.css';
|
||||
import './styles/backtestDashboard.css';
|
||||
|
||||
let _indCounter = 0;
|
||||
function newIndId() { return `ind_${++_indCounter}_${Date.now()}`; }
|
||||
@@ -611,15 +615,88 @@ function App() {
|
||||
const useUpbit = isUpbitMarket(symbol);
|
||||
|
||||
// ── 백테스팅 ───────────────────────────────────────────────────────────────
|
||||
const { signals: btSignals, stats: btStats, analysis: btAnalysis, running: btRunning, run: btRun, clear: btClear } = useBacktest();
|
||||
const {
|
||||
signals: btSignals,
|
||||
stats: btStats,
|
||||
analysis: btAnalysis,
|
||||
resultSymbol: btSymbol,
|
||||
resultTimeframe: btTimeframe,
|
||||
runSeq: btRunSeq,
|
||||
running: btRunning,
|
||||
run: btRun,
|
||||
clear: btClear,
|
||||
invalidateRuns: btInvalidateRuns,
|
||||
} = useBacktest();
|
||||
const [btSettings, setBtSettings] = useState<BacktestSettingsDto>(DEFAULT_BACKTEST_SETTINGS);
|
||||
const [showBtSettings, setShowBtSettings] = useState(false);
|
||||
const [showBtResult, setShowBtResult] = useState(false);
|
||||
const [btStrategyName, setBtStrategyName] = useState('전략');
|
||||
|
||||
/** 현재 차트와 일치하는 백테스팅 결과만 UI·마커에 반영 */
|
||||
const btMatchesChart = useMemo(
|
||||
() =>
|
||||
btSignals.length > 0 &&
|
||||
btSymbol != null &&
|
||||
btTimeframe != null &&
|
||||
btSymbol === symbol &&
|
||||
btTimeframe === timeframe,
|
||||
[btSignals.length, btSymbol, btTimeframe, symbol, timeframe],
|
||||
);
|
||||
|
||||
const btMarkerPayloadRef = useRef<{
|
||||
symbol: string;
|
||||
timeframe: string;
|
||||
signals: BacktestSignal[];
|
||||
showPrice: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const syncBacktestMarkersRef = useRef<() => void>(() => {});
|
||||
|
||||
/** ChartManager 메인 시리즈 준비 후 백테스트 마커 적용 (로딩·remount 직후 대응) */
|
||||
const applyBacktestMarkersWhenReady = useCallback((
|
||||
payload: NonNullable<typeof btMarkerPayloadRef.current>,
|
||||
attempt = 0,
|
||||
) => {
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr || !mgr.hasMainSeries()) {
|
||||
if (attempt < 30) {
|
||||
requestAnimationFrame(() => applyBacktestMarkersWhenReady(payload, attempt + 1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
btMarkerPayloadRef.current = payload;
|
||||
mgr.setBacktestMarkers(payload.signals, payload.showPrice);
|
||||
}, []);
|
||||
|
||||
const syncBacktestMarkers = useCallback(() => {
|
||||
const payload = btMarkerPayloadRef.current;
|
||||
if (
|
||||
payload &&
|
||||
payload.symbol === symbol &&
|
||||
payload.timeframe === timeframe &&
|
||||
payload.signals.length > 0
|
||||
) {
|
||||
applyBacktestMarkersWhenReady(payload);
|
||||
return;
|
||||
}
|
||||
managerRef.current?.clearBacktestMarkers();
|
||||
}, [symbol, timeframe, applyBacktestMarkersWhenReady]);
|
||||
|
||||
syncBacktestMarkersRef.current = syncBacktestMarkers;
|
||||
|
||||
// ── 실시간 전략 체크 (전역 설정 + DB 관심종목 = 체크 대상) ─────────────────
|
||||
const [showLivePanel, setShowLivePanel] = useState(false);
|
||||
const [liveStrategies, setLiveStrategies] = useState<{id:number;name:string}[]>([]);
|
||||
const [liveCandleType, setLiveCandleType] = useState('1m');
|
||||
|
||||
useEffect(() => {
|
||||
if (!appSettingsLoaded) return;
|
||||
loadLiveStrategySettings(symbol)
|
||||
.then(s => {
|
||||
if (s?.candleType) setLiveCandleType(s.candleType);
|
||||
})
|
||||
.catch(() => { /* 기본 1m 유지 */ });
|
||||
}, [symbol, appSettingsLoaded]);
|
||||
|
||||
const liveStrategySettings: LiveStrategySettingsDto = useMemo(() => ({
|
||||
market: symbol,
|
||||
@@ -627,7 +704,8 @@ function App() {
|
||||
isLiveCheck: appDefaults.liveStrategyCheck ?? false,
|
||||
executionType: appDefaults.liveExecutionType ?? 'CANDLE_CLOSE',
|
||||
positionMode: appDefaults.livePositionMode ?? 'LONG_ONLY',
|
||||
}), [symbol, appDefaults]);
|
||||
candleType: liveCandleType,
|
||||
}), [symbol, appDefaults, liveCandleType]);
|
||||
|
||||
/** 실시간 체크 ON + 전략 선택 시 STOMP 구독 대상 (관심종목 + 현재 차트 종목) */
|
||||
const monitoredMarkets = useMemo(() => {
|
||||
@@ -665,7 +743,15 @@ function App() {
|
||||
liveExecutionType: saved.executionType,
|
||||
livePositionMode: saved.positionMode,
|
||||
});
|
||||
}, [saveAppDef]);
|
||||
if (saved.candleType) setLiveCandleType(saved.candleType);
|
||||
if (appDefaults.liveStrategyCheck && saved.isLiveCheck) {
|
||||
loadActiveLiveStrategySettings()
|
||||
.then(list => setMarketSubscriptions(
|
||||
list.map(s => ({ market: s.market, candleType: s.candleType ?? '1m' })),
|
||||
))
|
||||
.catch(() => { /* 폴백 유지 */ });
|
||||
}
|
||||
}, [saveAppDef, appDefaults.liveStrategyCheck]);
|
||||
|
||||
const prevFavoritesRef = useRef<string[]>(getFavorites());
|
||||
|
||||
@@ -798,15 +884,37 @@ function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mainChartStyle]);
|
||||
|
||||
// 백테스팅 시그널 → ChartManager 마커 업데이트
|
||||
// 종목·타임프레임 변경 시 이전 백테스팅 결과·마커 제거 + 진행 중 run 무효화
|
||||
useEffect(() => {
|
||||
if (!managerRef.current) return;
|
||||
if (btSignals.length > 0) {
|
||||
managerRef.current.setBacktestMarkers(btSignals, appDefaults.btShowPrice);
|
||||
btInvalidateRuns();
|
||||
btMarkerPayloadRef.current = null;
|
||||
btClear();
|
||||
managerRef.current?.clearBacktestMarkers();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [symbol, timeframe]);
|
||||
|
||||
// 백테스팅 시그널 → ref 갱신 후 ChartManager 마커 동기화
|
||||
useEffect(() => {
|
||||
if (btMatchesChart && btSignals.length > 0) {
|
||||
btMarkerPayloadRef.current = {
|
||||
symbol,
|
||||
timeframe,
|
||||
signals: btSignals,
|
||||
showPrice: appDefaults.btShowPrice,
|
||||
};
|
||||
} else {
|
||||
managerRef.current.clearBacktestMarkers();
|
||||
btMarkerPayloadRef.current = null;
|
||||
}
|
||||
}, [btSignals, appDefaults.btShowPrice]);
|
||||
syncBacktestMarkers();
|
||||
let innerRaf = 0;
|
||||
const outerRaf = requestAnimationFrame(() => {
|
||||
innerRaf = requestAnimationFrame(syncBacktestMarkers);
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(outerRaf);
|
||||
if (innerRaf) cancelAnimationFrame(innerRaf);
|
||||
};
|
||||
}, [btMatchesChart, btSignals, btRunSeq, symbol, timeframe, appDefaults.btShowPrice, syncBacktestMarkers]);
|
||||
|
||||
// ── 시뮬레이션 데이터 (Upbit 아닐 때) ──────────────────────────────────
|
||||
const [simBars, setSimBars] = useState<OHLCVBar[]>(() =>
|
||||
@@ -1377,6 +1485,10 @@ function App() {
|
||||
<PaperTradingPage
|
||||
tickers={marketTickers}
|
||||
refreshKey={paperRefreshKey}
|
||||
defaultMarket={symbol}
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={() => setPaperRefreshKey(k => k + 1)}
|
||||
onGoChart={m => {
|
||||
goToMarketChart(m);
|
||||
setMenuPage('chart');
|
||||
@@ -1574,19 +1686,42 @@ function App() {
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
backtestActive={btSignals.length > 0}
|
||||
backtestActive={btMatchesChart}
|
||||
backtestRunning={btRunning}
|
||||
backtestRunningId={undefined}
|
||||
onRunBacktest={menuPage === 'chart' && layoutDef.count === 1
|
||||
? async (opts) => {
|
||||
if (opts.strategyName) setBtStrategyName(opts.strategyName);
|
||||
await btRun({ ...opts, bars, timeframe, settings: btSettings, symbol });
|
||||
if (useUpbit && (isLoading || bars.length === 0)) return;
|
||||
const runSymbol = symbol;
|
||||
const runTf = timeframe;
|
||||
const r = await btRun({
|
||||
...opts,
|
||||
bars,
|
||||
timeframe: runTf,
|
||||
settings: btSettings,
|
||||
symbol: runSymbol,
|
||||
});
|
||||
if (
|
||||
r &&
|
||||
r.symbol === runSymbol &&
|
||||
r.timeframe === runTf &&
|
||||
r.signals.length > 0
|
||||
) {
|
||||
const showPrice = appDefaults.btShowPrice;
|
||||
applyBacktestMarkersWhenReady({
|
||||
symbol: runSymbol,
|
||||
timeframe: runTf,
|
||||
signals: r.signals,
|
||||
showPrice,
|
||||
});
|
||||
}
|
||||
if (appDefaults.btAutoPopup) setShowBtResult(true);
|
||||
}
|
||||
: undefined}
|
||||
onOpenBtSettings={() => setShowBtSettings(true)}
|
||||
onOpenBtResults={() => setShowBtResult(true)}
|
||||
hasBtResult={btSignals.length > 0}
|
||||
hasBtResult={btMatchesChart}
|
||||
onClearBtMarkers={() => { btClear(); managerRef.current?.clearBacktestMarkers(); }}
|
||||
onToggleLiveStrategy={() => setShowLivePanel(v => !v)}
|
||||
liveStrategyActive={showLivePanel || monitoredMarkets.length > 0}
|
||||
@@ -1801,12 +1936,13 @@ function App() {
|
||||
drawingsVisible={drawingsVisible}
|
||||
onCrosshair={setLegend}
|
||||
onTradeOrderRequest={applyTradeFill}
|
||||
onDataLoaded={() => syncBacktestMarkersRef.current()}
|
||||
onManagerReady={mgr => {
|
||||
managerRef.current = mgr;
|
||||
const pending = pendingRealtimeBarRef.current;
|
||||
if (pending) {
|
||||
if (pending && bars.length > 0) {
|
||||
pendingRealtimeBarRef.current = null;
|
||||
const last = bars.length > 0 ? bars[bars.length - 1] : null;
|
||||
const last = bars[bars.length - 1];
|
||||
if (last && pending.time === last.time) mgr.updateBar(pending);
|
||||
else if (!last || pending.time > last.time) void mgr.appendBar(pending);
|
||||
else mgr.updateBar({ ...pending, time: last.time });
|
||||
@@ -1815,8 +1951,7 @@ function App() {
|
||||
const wrapper = document.querySelector('.chart-wrapper') as HTMLElement | null;
|
||||
mgr.setVolumeVisible(chartVolumeVisible, wrapper?.clientHeight);
|
||||
mgr.setDisplayTimezone(displayTimezone, timeframe);
|
||||
// 백테스팅 마커가 이미 있으면 즉시 적용
|
||||
if (btSignals.length > 0) mgr.setBacktestMarkers(btSignals, appDefaults.btShowPrice);
|
||||
syncBacktestMarkersRef.current();
|
||||
// 왼쪽 스크롤 감지 → 과거 데이터 추가 로드
|
||||
mgr.subscribeVisibleLogicalRange(r => {
|
||||
if (r && r.from < LOAD_MORE_TRIGGER) {
|
||||
@@ -1857,7 +1992,7 @@ function App() {
|
||||
/>
|
||||
|
||||
{/* 백테스팅 결과 통계 배지 */}
|
||||
{btSignals.length > 0 && layoutDef.count === 1 && (
|
||||
{btMatchesChart && layoutDef.count === 1 && (
|
||||
<BacktestPanel
|
||||
stats={btStats}
|
||||
signalCount={btSignals.length}
|
||||
|
||||
Reference in New Issue
Block a user