전략평가 화면 수익률 요약표시
This commit is contained in:
@@ -48,8 +48,14 @@ import { resolveBarSignalHighlight } from '../utils/strategyEvaluationBarSignals
|
||||
import type { OHLCVBar } from '../types';
|
||||
import BacktestAnalysisReportModal from './backtest/BacktestAnalysisReportModal';
|
||||
import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal';
|
||||
import { buildStrategyEvaluationReportModel } from '../utils/strategyEvaluationReport';
|
||||
import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup';
|
||||
import {
|
||||
buildStrategyEvaluationReportModel,
|
||||
buildStrategyEvaluationToolbarSummary,
|
||||
formatEvaluationReturnPct,
|
||||
padEvaluationSignalCount,
|
||||
} from '../utils/strategyEvaluationReport';
|
||||
import { BACKTEST_DISPLAY_BAR_COUNT, resolveEvaluationBarSlice } from '../utils/backtestWarmup';
|
||||
import type { StrategyEvaluationWindowMeta } from './strategyEvaluation/StrategyEvaluationChart';
|
||||
import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
|
||||
import StrategyEvaluationChartSettingsTab from './strategyEvaluation/StrategyEvaluationChartSettingsTab';
|
||||
import PaletteDragOverlay from './strategyEditor/PaletteDragOverlay';
|
||||
@@ -104,6 +110,12 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
const [timeframeChoice, setTimeframeChoice] = useState<BacktestRunTimeframeChoice>(BACKTEST_STRATEGY_TIMEFRAME);
|
||||
|
||||
const [bars, setBars] = useState<OHLCVBar[]>([]);
|
||||
const [evalWindowMeta, setEvalWindowMeta] = useState<StrategyEvaluationWindowMeta>({
|
||||
evaluationBarCount: BACKTEST_DISPLAY_BAR_COUNT,
|
||||
warmupBarCount: 0,
|
||||
isAtLatestWindow: true,
|
||||
windowEndTimeSec: null,
|
||||
});
|
||||
const [selectedBarIndex, setSelectedBarIndex] = useState(0);
|
||||
const [snapshot, setSnapshot] = useState<VirtualIndicatorSnapshot | undefined>();
|
||||
const [evalLoading, setEvalLoading] = useState(false);
|
||||
@@ -112,6 +124,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
const [paramsRevision, setParamsRevision] = useState(0);
|
||||
const [chartLoading, setChartLoading] = useState(false);
|
||||
const [backtestSignals, setBacktestSignals] = useState<BacktestSignal[]>([]);
|
||||
const [signalScanRunning, setSignalScanRunning] = useState(false);
|
||||
const [initialCapital, setInitialCapital] = useState(10_000_000);
|
||||
const [editorModalOpen, setEditorModalOpen] = useState(false);
|
||||
const [reportOpen, setReportOpen] = useState(false);
|
||||
const [reportModel, setReportModel] = useState<BacktestAnalysisReportModel | null>(null);
|
||||
@@ -385,6 +399,20 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
setBacktestSignals(signals);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadBacktestSettings().then(s => {
|
||||
if (s?.initialCapital) setInitialCapital(s.initialCapital);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const analysisSummary = useMemo(() => {
|
||||
if (!selectedStrategy) return null;
|
||||
return buildStrategyEvaluationToolbarSummary(backtestSignals, {
|
||||
initialCapital,
|
||||
symbol: market.replace(/^KRW-/, ''),
|
||||
});
|
||||
}, [selectedStrategy, backtestSignals, initialCapital, market]);
|
||||
|
||||
const refreshEvaluation = useCallback(async () => {
|
||||
if (!selectedStrategyId || !selectedStrategy) {
|
||||
setSnapshot(undefined);
|
||||
@@ -488,8 +516,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
const reportCacheKey = useMemo(() => {
|
||||
if (!selectedStrategyId || bars.length < 10) return null;
|
||||
const lastBarTime = bars[bars.length - 1]?.time ?? 0;
|
||||
return `${selectedStrategyId}|${market}|${chartTimeframe}|${paramsRevision}|${bars.length}|${lastBarTime}`;
|
||||
}, [selectedStrategyId, market, chartTimeframe, paramsRevision, bars]);
|
||||
const windowKey = evalWindowMeta.windowEndTimeSec ?? 'latest';
|
||||
return `${selectedStrategyId}|${market}|${chartTimeframe}|${paramsRevision}|${bars.length}|${lastBarTime}|${windowKey}`;
|
||||
}, [selectedStrategyId, market, chartTimeframe, paramsRevision, bars, evalWindowMeta.windowEndTimeSec]);
|
||||
|
||||
const reportDisabled = !selectedStrategyId || !selectedStrategy || bars.length < 10 || chartLoading;
|
||||
|
||||
@@ -517,6 +546,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
|
||||
try {
|
||||
const indicatorParams = buildEvalParamsFromStrategy(selectedStrategy, getEvalParams);
|
||||
const evaluationBarCount = evalWindowMeta.evaluationBarCount;
|
||||
const context = buildStrategyEvaluationAiVerifyContext({
|
||||
strategy: selectedStrategy,
|
||||
strategyId: selectedStrategyId,
|
||||
@@ -530,7 +560,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
chartSignals: backtestSignals,
|
||||
barSignalHighlight,
|
||||
indicatorParams,
|
||||
evaluationBarCount: Math.min(BACKTEST_DISPLAY_BAR_COUNT, bars.length),
|
||||
evaluationBarCount,
|
||||
});
|
||||
setAiVerifyContext(context);
|
||||
|
||||
@@ -568,6 +598,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
backtestSignals,
|
||||
barSignalHighlight,
|
||||
getEvalParams,
|
||||
evalWindowMeta.evaluationBarCount,
|
||||
]);
|
||||
|
||||
const handleOpenAiVerify = useCallback(() => {
|
||||
@@ -590,7 +621,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
try {
|
||||
const [btSettings] = await Promise.all([loadBacktestSettings()]);
|
||||
const indicatorParams = buildEvalParamsFromStrategy(selectedStrategy, getEvalParams);
|
||||
const evaluationBarCount = Math.min(BACKTEST_DISPLAY_BAR_COUNT, bars.length);
|
||||
const evaluationBarCount = evalWindowMeta.evaluationBarCount;
|
||||
const { evalBars } = resolveEvaluationBarSlice(bars, evaluationBarCount);
|
||||
const res = await runBacktest({
|
||||
strategyId: selectedStrategyId,
|
||||
bars: bars.map(b => ({
|
||||
@@ -621,9 +653,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
strategyName: selectedStrategy.name ?? strategyLabel,
|
||||
symbol: market.replace(/^KRW-/, ''),
|
||||
timeframe: chartTimeframe,
|
||||
barCount: bars.length,
|
||||
firstBarClose: bars[0]?.close,
|
||||
lastBarClose: bars[bars.length - 1]?.close,
|
||||
barCount: evaluationBarCount,
|
||||
firstBarClose: evalBars[0]?.close,
|
||||
lastBarClose: evalBars[evalBars.length - 1]?.close,
|
||||
});
|
||||
if (!model) {
|
||||
window.alert('분석 데이터를 생성할 수 없습니다.');
|
||||
@@ -650,6 +682,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
chartTimeframe,
|
||||
market,
|
||||
strategyLabel,
|
||||
evalWindowMeta.evaluationBarCount,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -659,6 +692,45 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
<h1 className="btd-header-title">전략 평가</h1>
|
||||
<span className="btd-header-sub">Strategy Match Analysis</span>
|
||||
</div>
|
||||
<div className="btd-header-actions seval-header-analysis">
|
||||
{selectedStrategy && signalScanRunning && (
|
||||
<span className="seval-header-analysis-busy">시그널 계산 중…</span>
|
||||
)}
|
||||
{selectedStrategy && analysisSummary && !signalScanRunning && (
|
||||
<span
|
||||
className="seval-analysis-summary"
|
||||
title="일괄매수(최초 전액) · 분할매수(40/30/잔여) · 레포트와 동일 필터 기준"
|
||||
>
|
||||
<span className="seval-analysis-item">
|
||||
매수:<strong>{padEvaluationSignalCount(analysisSummary.buyCount)}</strong>건
|
||||
</span>
|
||||
<span className="seval-analysis-sep" aria-hidden>·</span>
|
||||
<span className="seval-analysis-item">
|
||||
매도:<strong>{padEvaluationSignalCount(analysisSummary.sellCount)}</strong>건
|
||||
</span>
|
||||
<span className="seval-analysis-sep" aria-hidden>·</span>
|
||||
<span className={`seval-analysis-item seval-analysis-return${
|
||||
analysisSummary.fullBuyReturnPct > 0
|
||||
? ' seval-analysis-return--pos'
|
||||
: analysisSummary.fullBuyReturnPct < 0
|
||||
? ' seval-analysis-return--neg'
|
||||
: ''
|
||||
}`}>
|
||||
일괄:<strong>{formatEvaluationReturnPct(analysisSummary.fullBuyReturnPct)}</strong>
|
||||
</span>
|
||||
<span className="seval-analysis-sep" aria-hidden>·</span>
|
||||
<span className={`seval-analysis-item seval-analysis-return${
|
||||
analysisSummary.splitBuyReturnPct > 0
|
||||
? ' seval-analysis-return--pos'
|
||||
: analysisSummary.splitBuyReturnPct < 0
|
||||
? ' seval-analysis-return--neg'
|
||||
: ''
|
||||
}`}>
|
||||
분할:<strong>{formatEvaluationReturnPct(analysisSummary.splitBuyReturnPct)}</strong>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="btd-body">
|
||||
@@ -819,12 +891,14 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
selectedBarIndex={selectedBarIndex}
|
||||
onSelectedBarIndexChange={setSelectedBarIndex}
|
||||
onBarsLoaded={setBars}
|
||||
onEvalWindowMetaChange={setEvalWindowMeta}
|
||||
paramsRevision={paramsRevision}
|
||||
appliedIndicatorParams={appliedIndicatorParams}
|
||||
getParamsOverride={getEvalParams}
|
||||
getVisualConfigOverride={getVisualConfig}
|
||||
evalLoading={evalLoading}
|
||||
onSignalsChange={handleSignalsChange}
|
||||
onBacktestRunningChange={setSignalScanRunning}
|
||||
onOpenReport={() => { void handleOpenReport(); }}
|
||||
reportDisabled={reportDisabled}
|
||||
reportLoading={reportLoading}
|
||||
|
||||
@@ -19,6 +19,8 @@ import { fetchLiveConditionScanSignals } from '../../utils/backendApi';
|
||||
import type { MarketInfo, TickerData } from '../../hooks/useMarketTicker';
|
||||
import {
|
||||
BACKTEST_DISPLAY_BAR_COUNT,
|
||||
computeNextWindowEndTime,
|
||||
computePreviousWindowEndTime,
|
||||
fetchBacktestCandleBundle,
|
||||
} from '../../utils/backtestWarmup';
|
||||
import type { EvalIndicatorParams } from '../../utils/strategyEvaluationParams';
|
||||
@@ -52,6 +54,13 @@ import {
|
||||
} from '../../utils/backtestRunTimeframe';
|
||||
import { timeframeBarSeconds } from '../../utils/backtestUiUtils';
|
||||
|
||||
export interface StrategyEvaluationWindowMeta {
|
||||
evaluationBarCount: number;
|
||||
warmupBarCount: number;
|
||||
isAtLatestWindow: boolean;
|
||||
windowEndTimeSec: number | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
timeframe: Timeframe;
|
||||
@@ -68,6 +77,7 @@ interface Props {
|
||||
selectedBarIndex: number;
|
||||
onSelectedBarIndexChange: (index: number) => void;
|
||||
onBarsLoaded?: (bars: OHLCVBar[]) => void;
|
||||
onEvalWindowMetaChange?: (meta: StrategyEvaluationWindowMeta) => void;
|
||||
paramsRevision?: number;
|
||||
/** 사용자가 저장한 지표 파라미터 — null 이면 백테스트도 서버 DB 설정 사용 (live-conditions 와 동일) */
|
||||
appliedIndicatorParams?: EvalIndicatorParams | null;
|
||||
@@ -88,6 +98,7 @@ interface Props {
|
||||
evalLoading?: boolean;
|
||||
/** 백테스트 시그널 목록 변경 시 (우측 패널 하이라이트 등) */
|
||||
onSignalsChange?: (signals: BacktestSignal[]) => void;
|
||||
onBacktestRunningChange?: (running: boolean) => void;
|
||||
/** 투자분석 상세 레포트 (인쇄·PDF) */
|
||||
onOpenReport?: () => void;
|
||||
reportDisabled?: boolean;
|
||||
@@ -132,12 +143,14 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
selectedBarIndex,
|
||||
onSelectedBarIndexChange,
|
||||
onBarsLoaded,
|
||||
onEvalWindowMetaChange,
|
||||
paramsRevision = 0,
|
||||
appliedIndicatorParams = null,
|
||||
getParamsOverride,
|
||||
getVisualConfigOverride,
|
||||
evalLoading = false,
|
||||
onSignalsChange,
|
||||
onBacktestRunningChange,
|
||||
onOpenReport,
|
||||
reportDisabled = false,
|
||||
reportLoading = false,
|
||||
@@ -181,6 +194,11 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
|
||||
const backtestGenRef = useRef(0);
|
||||
const evaluationBarCountRef = useRef(BACKTEST_DISPLAY_BAR_COUNT);
|
||||
const warmupBarCountRef = useRef(0);
|
||||
const latestBarTimeSecRef = useRef<number | null>(null);
|
||||
type EvaluationWindowAnchor = 'latest' | number;
|
||||
const [windowAnchor, setWindowAnchor] = useState<EvaluationWindowAnchor>('latest');
|
||||
const [displayEvalBarCount, setDisplayEvalBarCount] = useState(BACKTEST_DISPLAY_BAR_COUNT);
|
||||
const signalsRef = useRef(signals);
|
||||
signalsRef.current = signals;
|
||||
const chartType: ChartType = 'candlestick';
|
||||
@@ -428,8 +446,25 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
}, [signals, onSignalsChange]);
|
||||
|
||||
useEffect(() => {
|
||||
onChartLoadingChange?.(loading);
|
||||
}, [loading, onChartLoadingChange]);
|
||||
onBacktestRunningChange?.(backtestRunning);
|
||||
}, [backtestRunning, onBacktestRunningChange]);
|
||||
|
||||
const emitEvalWindowMeta = useCallback((
|
||||
evalCount: number,
|
||||
warmupCount: number,
|
||||
anchor: EvaluationWindowAnchor,
|
||||
) => {
|
||||
onEvalWindowMetaChange?.({
|
||||
evaluationBarCount: evalCount,
|
||||
warmupBarCount: warmupCount,
|
||||
isAtLatestWindow: anchor === 'latest',
|
||||
windowEndTimeSec: anchor === 'latest' ? null : anchor,
|
||||
});
|
||||
}, [onEvalWindowMetaChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setWindowAnchor('latest');
|
||||
}, [market, timeframe, strategy?.id, paramsRevision]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -440,6 +475,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
managerRef.current?.clearBacktestMarkers();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const toTimeSec = windowAnchor === 'latest' ? undefined : windowAnchor;
|
||||
void fetchBacktestCandleBundle({
|
||||
market,
|
||||
timeframe,
|
||||
@@ -447,12 +484,19 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
buyDsl: strategy?.buyCondition,
|
||||
sellDsl: strategy?.sellCondition,
|
||||
indicatorParams,
|
||||
toTimeSec,
|
||||
})
|
||||
.then(({ bars: data, evaluationBarCount: evalCount }) => {
|
||||
.then(({ bars: data, evaluationBarCount: evalCount, warmupBarCount }) => {
|
||||
if (cancelled) return;
|
||||
evaluationBarCountRef.current = evalCount;
|
||||
warmupBarCountRef.current = warmupBarCount;
|
||||
setDisplayEvalBarCount(evalCount);
|
||||
if (windowAnchor === 'latest') {
|
||||
latestBarTimeSecRef.current = data[data.length - 1]?.time ?? null;
|
||||
}
|
||||
setBars(data);
|
||||
onBarsLoaded?.(data);
|
||||
emitEvalWindowMeta(evalCount, warmupBarCount, windowAnchor);
|
||||
if (data.length > 0) {
|
||||
onSelectedBarIndexChange(data.length - 1);
|
||||
}
|
||||
@@ -466,7 +510,17 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [market, timeframe, strategy?.id, paramsRevision, onBarsLoaded, onSelectedBarIndexChange]);
|
||||
}, [
|
||||
market,
|
||||
timeframe,
|
||||
strategy?.id,
|
||||
paramsRevision,
|
||||
indicatorParamsKey,
|
||||
windowAnchor,
|
||||
onBarsLoaded,
|
||||
onSelectedBarIndexChange,
|
||||
emitEvalWindowMeta,
|
||||
]);
|
||||
|
||||
const scrollToBar = useCallback((barIdx: number) => {
|
||||
const mgr = managerRef.current;
|
||||
@@ -523,6 +577,31 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
onSelectedBarIndexChange(bars.length - 1);
|
||||
}, [bars.length, onSelectedBarIndexChange]);
|
||||
|
||||
const isAtLatestWindow = windowAnchor === 'latest';
|
||||
|
||||
const shiftEvaluationWindow = useCallback((direction: 'prev' | 'next') => {
|
||||
if (loading || bars.length === 0) return;
|
||||
const evalCount = evaluationBarCountRef.current;
|
||||
const barSec = timeframeBarSeconds(timeframe);
|
||||
|
||||
if (direction === 'next') {
|
||||
if (isAtLatestWindow) return;
|
||||
const next = computeNextWindowEndTime(
|
||||
bars,
|
||||
evalCount,
|
||||
latestBarTimeSecRef.current,
|
||||
barSec,
|
||||
);
|
||||
if (next == null) return;
|
||||
setWindowAnchor(next === 'latest' ? 'latest' : next);
|
||||
return;
|
||||
}
|
||||
|
||||
const prevEnd = computePreviousWindowEndTime(bars, evalCount, barSec);
|
||||
if (prevEnd == null) return;
|
||||
setWindowAnchor(prevEnd);
|
||||
}, [loading, bars, timeframe, isAtLatestWindow]);
|
||||
|
||||
useEffect(() => {
|
||||
const mgr = chartMgr;
|
||||
if (!mgr || bars.length === 0) return;
|
||||
@@ -601,6 +680,10 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
onMarketSearchOpenChange?.(showMarketSearch);
|
||||
}, [showMarketSearch, onMarketSearchOpenChange]);
|
||||
|
||||
useEffect(() => {
|
||||
onChartLoadingChange?.(loading);
|
||||
}, [loading, onChartLoadingChange]);
|
||||
|
||||
const selectedBar = bars[selectedBarIndex] ?? null;
|
||||
|
||||
return (
|
||||
@@ -655,11 +738,6 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
{new Date(selectedBar.time * 1000).toLocaleString('ko-KR')}
|
||||
</span>
|
||||
)}
|
||||
{strategy && signals.length > 0 && !backtestRunning && (
|
||||
<span className="seval-chart-signal-count" title="조건 충족 매매 시그널">
|
||||
시그널 {signals.length}건
|
||||
</span>
|
||||
)}
|
||||
{strategy && backtestRunning && (
|
||||
<span className="seval-chart-backtest-busy">시그널 계산 중…</span>
|
||||
)}
|
||||
@@ -818,37 +896,67 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
||||
</div>
|
||||
|
||||
<div className="seval-chart-bottom-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
className="btd-btn btd-btn--ghost seval-chart-nav--step"
|
||||
title="이전 봉 (←)"
|
||||
onClick={() => stepBar(-1)}
|
||||
disabled={selectedBarIndex <= 0 || bars.length === 0}
|
||||
>
|
||||
◀ 이전 봉
|
||||
</button>
|
||||
<span className="seval-chart-count">
|
||||
{bars.length > 0 ? `${selectedBarIndex + 1} / ${bars.length}` : '0봉'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btd-btn btd-btn--ghost seval-chart-nav--step"
|
||||
title="다음 봉 (→)"
|
||||
onClick={() => stepBar(1)}
|
||||
disabled={selectedBarIndex >= bars.length - 1 || bars.length === 0}
|
||||
>
|
||||
다음 봉 ▶
|
||||
</button>
|
||||
<span className="seval-chart-bottom-divider" aria-hidden />
|
||||
<button
|
||||
type="button"
|
||||
className="btd-btn btd-btn--ghost"
|
||||
title="최신 봉"
|
||||
onClick={goLatest}
|
||||
disabled={bars.length === 0 || selectedBarIndex >= bars.length - 1}
|
||||
>
|
||||
최신
|
||||
</button>
|
||||
<div className="seval-chart-bottom-side seval-chart-bottom-side--left">
|
||||
<button
|
||||
type="button"
|
||||
className="btd-btn btd-btn--ghost seval-chart-nav--window"
|
||||
title={`이전 ${BACKTEST_DISPLAY_BAR_COUNT}봉 구간 평가 (워밍업 포함 재조회)`}
|
||||
onClick={() => shiftEvaluationWindow('prev')}
|
||||
disabled={loading || bars.length === 0}
|
||||
>
|
||||
◀◀ 이전봉 평가
|
||||
</button>
|
||||
</div>
|
||||
<div className="seval-chart-bottom-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btd-btn btd-btn--ghost seval-chart-nav--step"
|
||||
title="이전 봉 (←)"
|
||||
onClick={() => stepBar(-1)}
|
||||
disabled={selectedBarIndex <= 0 || bars.length === 0}
|
||||
>
|
||||
◀ 이전 봉
|
||||
</button>
|
||||
<span className="seval-chart-count">
|
||||
{bars.length > 0 ? `${selectedBarIndex + 1} / ${bars.length}` : '0봉'}
|
||||
{bars.length > 0 && (
|
||||
<span className="seval-chart-eval-range">
|
||||
{isAtLatestWindow ? ' · 최신 평가' : ' · 과거 평가'}
|
||||
{` · ${Math.min(displayEvalBarCount, bars.length)}봉`}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btd-btn btd-btn--ghost seval-chart-nav--step"
|
||||
title="다음 봉 (→)"
|
||||
onClick={() => stepBar(1)}
|
||||
disabled={selectedBarIndex >= bars.length - 1 || bars.length === 0}
|
||||
>
|
||||
다음 봉 ▶
|
||||
</button>
|
||||
<span className="seval-chart-bottom-divider" aria-hidden />
|
||||
<button
|
||||
type="button"
|
||||
className="btd-btn btd-btn--ghost"
|
||||
title="최신 봉"
|
||||
onClick={goLatest}
|
||||
disabled={bars.length === 0 || selectedBarIndex >= bars.length - 1}
|
||||
>
|
||||
최신
|
||||
</button>
|
||||
</div>
|
||||
<div className="seval-chart-bottom-side seval-chart-bottom-side--right">
|
||||
<button
|
||||
type="button"
|
||||
className="btd-btn btd-btn--ghost seval-chart-nav--window"
|
||||
title={`다음 ${BACKTEST_DISPLAY_BAR_COUNT}봉 구간 평가`}
|
||||
onClick={() => shiftEvaluationWindow('next')}
|
||||
disabled={loading || bars.length === 0 || isAtLatestWindow}
|
||||
>
|
||||
이후봉 평가 ▶▶
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showMarketSearch && marketBtnRef.current && ReactDOM.createPortal(
|
||||
|
||||
Reference in New Issue
Block a user