커스텀 수정
This commit is contained in:
@@ -14,7 +14,13 @@ import {
|
|||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import type { Theme } from '../types';
|
import type { Theme } from '../types';
|
||||||
import { buildEquityFromSignals } from '../utils/backtestEquity';
|
import { buildEquityFromSignals } from '../utils/backtestEquity';
|
||||||
import { normalizeEpochSec, normalizeEpochSecOptional } from '../utils/backtestUiUtils';
|
import {
|
||||||
|
formatChartTypeKo,
|
||||||
|
formatTimeframeKo,
|
||||||
|
normalizeEpochSec,
|
||||||
|
normalizeEpochSecOptional,
|
||||||
|
toUpbitMarket,
|
||||||
|
} from '../utils/backtestUiUtils';
|
||||||
import BacktestExecutionList, { type ExecutionListTab } from './backtest/BacktestExecutionList';
|
import BacktestExecutionList, { type ExecutionListTab } from './backtest/BacktestExecutionList';
|
||||||
import BacktestAnalysisChart from './backtest/BacktestAnalysisChart';
|
import BacktestAnalysisChart from './backtest/BacktestAnalysisChart';
|
||||||
import BacktestKpiPanel from './backtest/BacktestKpiPanel';
|
import BacktestKpiPanel from './backtest/BacktestKpiPanel';
|
||||||
@@ -29,10 +35,6 @@ import {
|
|||||||
import '../styles/strategyEditorTheme.css';
|
import '../styles/strategyEditorTheme.css';
|
||||||
import { getKoreanName } from '../utils/marketNameCache';
|
import { getKoreanName } from '../utils/marketNameCache';
|
||||||
|
|
||||||
function toUpbitMarket(symbol: string): string {
|
|
||||||
return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LEFT_KEY = 'btd-left-width';
|
const LEFT_KEY = 'btd-left-width';
|
||||||
const RIGHT_KEY = 'btd-right-width';
|
const RIGHT_KEY = 'btd-right-width';
|
||||||
const LEFT_DEFAULT = 380;
|
const LEFT_DEFAULT = 380;
|
||||||
@@ -199,7 +201,10 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
return {
|
return {
|
||||||
ko: getKoreanName(market),
|
ko: getKoreanName(market),
|
||||||
strategy: selectedBacktest.strategyName || '전략 없음',
|
strategy: selectedBacktest.strategyName || '전략 없음',
|
||||||
meta: selectedBacktest.timeframe,
|
metaParts: [
|
||||||
|
formatTimeframeKo(selectedBacktest.timeframe),
|
||||||
|
formatChartTypeKo('candlestick'),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (tab === 'live' && selectedLive) {
|
if (tab === 'live' && selectedLive) {
|
||||||
@@ -207,11 +212,15 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
return {
|
return {
|
||||||
ko: getKoreanName(market),
|
ko: getKoreanName(market),
|
||||||
strategy: selectedLive.strategyLabel,
|
strategy: selectedLive.strategyLabel,
|
||||||
meta: selectedLive.sourceLabel,
|
metaParts: [
|
||||||
|
formatTimeframeKo(chartProps?.timeframe ?? '1h'),
|
||||||
|
formatChartTypeKo('candlestick'),
|
||||||
|
selectedLive.sourceLabel,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [tab, selectedBacktest, selectedLive]);
|
}, [tab, selectedBacktest, selectedLive, chartProps]);
|
||||||
|
|
||||||
const footerTrades = equityData.trades;
|
const footerTrades = equityData.trades;
|
||||||
|
|
||||||
@@ -276,7 +285,12 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
<h2 className="btd-main-ko">{centerHead.ko}</h2>
|
<h2 className="btd-main-ko">{centerHead.ko}</h2>
|
||||||
<p className="btd-main-strategy">
|
<p className="btd-main-strategy">
|
||||||
{centerHead.strategy}
|
{centerHead.strategy}
|
||||||
{centerHead.meta ? <span className="btd-main-meta"> · {centerHead.meta}</span> : null}
|
{centerHead.metaParts.length > 0 && (
|
||||||
|
<span className="btd-main-meta">
|
||||||
|
{' · '}
|
||||||
|
{centerHead.metaParts.filter(Boolean).join(' · ')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ import type { ChartManager } from '../../utils/ChartManager';
|
|||||||
import { useHistoryLoader, LOAD_MORE_TRIGGER } from '../../hooks/useHistoryLoader';
|
import { useHistoryLoader, LOAD_MORE_TRIGGER } from '../../hooks/useHistoryLoader';
|
||||||
import { isUpbitMarket } from '../../utils/upbitApi';
|
import { isUpbitMarket } from '../../utils/upbitApi';
|
||||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
||||||
import { normalizeChartTimeframe, timeframeBarSeconds } from '../../utils/backtestUiUtils';
|
import {
|
||||||
|
formatChartTypeKo,
|
||||||
|
formatTimeframeKo,
|
||||||
|
normalizeChartTimeframe,
|
||||||
|
timeframeBarSeconds,
|
||||||
|
} from '../../utils/backtestUiUtils';
|
||||||
|
import { getKoreanName } from '../../utils/marketNameCache';
|
||||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||||
import { useAppSettings } from '../../hooks/useAppSettings';
|
import { useAppSettings } from '../../hooks/useAppSettings';
|
||||||
import { enrichIndicatorConfig, getIndicatorDef } from '../../utils/indicatorRegistry';
|
import { enrichIndicatorConfig, getIndicatorDef } from '../../utils/indicatorRegistry';
|
||||||
@@ -294,7 +300,9 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
if (mgr) applyPaneRatio(mgr);
|
if (mgr) applyPaneRatio(mgr);
|
||||||
}, [paneAreaRatio, applyPaneRatio]);
|
}, [paneAreaRatio, applyPaneRatio]);
|
||||||
|
|
||||||
const symLabel = market.replace(/^KRW-/, '');
|
const koName = getKoreanName(market);
|
||||||
|
const tfKo = formatTimeframeKo(chartTimeframe);
|
||||||
|
const chartTypeKo = formatChartTypeKo(chartType);
|
||||||
const stratTf = chartTimeframe;
|
const stratTf = chartTimeframe;
|
||||||
|
|
||||||
const priceLabels = appDefaults.chartSeriesPriceLabels !== false;
|
const priceLabels = appDefaults.chartSeriesPriceLabels !== false;
|
||||||
@@ -303,9 +311,9 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
<div className="btd-analysis-chart">
|
<div className="btd-analysis-chart">
|
||||||
<div className="btd-analysis-toolbar">
|
<div className="btd-analysis-toolbar">
|
||||||
<div className="btd-analysis-toolbar-left">
|
<div className="btd-analysis-toolbar-left">
|
||||||
<span className="btd-analysis-select btd-analysis-select--static">{symLabel}</span>
|
<span className="btd-analysis-select btd-analysis-select--static">{koName}</span>
|
||||||
<span className="btd-analysis-select btd-analysis-select--static">{chartTimeframe}</span>
|
<span className="btd-analysis-select btd-analysis-select--static">{tfKo}</span>
|
||||||
<span className="btd-analysis-select btd-analysis-select--static">Candle</span>
|
<span className="btd-analysis-select btd-analysis-select--static">{chartTypeKo}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="btd-analysis-toolbar-right">
|
<div className="btd-analysis-toolbar-right">
|
||||||
<span className="btd-analysis-tool" title="자석 모드">⊕</span>
|
<span className="btd-analysis-tool" title="자석 모드">⊕</span>
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import type { LiveExecutionItem } from '../../utils/liveExecutionGroups';
|
|||||||
import BacktestSparkline from './BacktestSparkline';
|
import BacktestSparkline from './BacktestSparkline';
|
||||||
import { buildEquityFromSignals } from '../../utils/backtestEquity';
|
import { buildEquityFromSignals } from '../../utils/backtestEquity';
|
||||||
import { paperTradesToSignals } from '../../utils/liveExecutionGroups';
|
import { paperTradesToSignals } from '../../utils/liveExecutionGroups';
|
||||||
import { fmtListTimestamp, fmtShortTimestamp, pct, pctAbs } from '../../utils/backtestUiUtils';
|
import {
|
||||||
|
fmtListTimestamp,
|
||||||
|
fmtShortTimestamp,
|
||||||
|
formatTimeframeKo,
|
||||||
|
pct,
|
||||||
|
pctAbs,
|
||||||
|
toUpbitMarket,
|
||||||
|
} from '../../utils/backtestUiUtils';
|
||||||
import { getKoreanName } from '../../utils/marketNameCache';
|
import { getKoreanName } from '../../utils/marketNameCache';
|
||||||
|
|
||||||
function toUpbitMarket(symbol: string): string {
|
|
||||||
return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExecutionListTab = 'backtest' | 'live';
|
export type ExecutionListTab = 'backtest' | 'live';
|
||||||
export type BacktestListSort = 'strategy' | 'symbol' | 'return';
|
export type BacktestListSort = 'strategy' | 'symbol' | 'return';
|
||||||
|
|
||||||
@@ -95,7 +98,7 @@ export default function BacktestExecutionList({
|
|||||||
<div className="btd-history-card-row">
|
<div className="btd-history-card-row">
|
||||||
<div className="btd-history-card-main">
|
<div className="btd-history-card-main">
|
||||||
<span className="btd-history-ko">{ko}</span>
|
<span className="btd-history-ko">{ko}</span>
|
||||||
<span className="btd-history-strategy">{strategy} · {r.timeframe}</span>
|
<span className="btd-history-strategy">{strategy} · {formatTimeframeKo(r.timeframe)}</span>
|
||||||
<span className="btd-history-date">{fmtListTimestamp(r.createdAt)}</span>
|
<span className="btd-history-date">{fmtListTimestamp(r.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="btd-history-card-side">
|
<div className="btd-history-card-side">
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
type ChartCustomOverlaySlotId,
|
type ChartCustomOverlaySlotId,
|
||||||
createDefaultChartCustomOverlaySelection,
|
createDefaultChartCustomOverlaySelection,
|
||||||
isCustomIchimokuCloudVisible,
|
isCustomIchimokuCloudVisible,
|
||||||
|
isCustomOverlayPlotControlledType,
|
||||||
isCustomPlotSelected,
|
isCustomPlotSelected,
|
||||||
} from './chartCustomOverlay';
|
} from './chartCustomOverlay';
|
||||||
import type { PlotDef, HLineStyle } from './indicatorRegistry';
|
import type { PlotDef, HLineStyle } from './indicatorRegistry';
|
||||||
@@ -1372,12 +1373,14 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(_config, info.lastVal, pane);
|
const showPriceLabel = this.shouldShowSeriesPriceLabel(_config, info.lastVal, pane);
|
||||||
|
const lineVisible = this._resolveSeriesVisible(_config, info.id);
|
||||||
const series = this.chart.addSeries(LineSeries, {
|
const series = this.chart.addSeries(LineSeries, {
|
||||||
color: info.color,
|
color: info.color,
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
lastValueVisible: showPriceLabel,
|
lastValueVisible: showPriceLabel && lineVisible,
|
||||||
title: showPriceLabel ? info.title : '',
|
title: showPriceLabel && lineVisible ? info.title : '',
|
||||||
|
visible: lineVisible,
|
||||||
}, pane);
|
}, pane);
|
||||||
series.setData(seriesData);
|
series.setData(seriesData);
|
||||||
seriesList.push(series);
|
seriesList.push(series);
|
||||||
@@ -2435,9 +2438,13 @@ export class ChartManager {
|
|||||||
|
|
||||||
/** 사용자 hidden + 세션 오버레이 토글 — entry.config.hidden 은 사용자 의도만 반영 */
|
/** 사용자 hidden + 세션 오버레이 토글 — entry.config.hidden 은 사용자 의도만 반영 */
|
||||||
private _isIndicatorEffectivelyVisible(config: IndicatorConfig): boolean {
|
private _isIndicatorEffectivelyVisible(config: IndicatorConfig): boolean {
|
||||||
|
if (config.hidden === true) return false;
|
||||||
|
if (this._isCustomOverlayApplied() && isCustomOverlayPlotControlledType(config.type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const overlayKey = chartOverlayKeyForType(config.type);
|
const overlayKey = chartOverlayKeyForType(config.type);
|
||||||
if (overlayKey && !this._chartOverlayVisibility[overlayKey]) return false;
|
if (overlayKey && !this._chartOverlayVisibility[overlayKey]) return false;
|
||||||
return config.hidden !== true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChartCustomOverlayActive(slot: ChartCustomOverlaySlotId = 'custom'): boolean {
|
getChartCustomOverlayActive(slot: ChartCustomOverlaySlotId = 'custom'): boolean {
|
||||||
@@ -2458,14 +2465,15 @@ export class ChartManager {
|
|||||||
selection: ChartCustomOverlaySelection,
|
selection: ChartCustomOverlaySelection,
|
||||||
slot: ChartCustomOverlaySlotId = 'custom',
|
slot: ChartCustomOverlaySlotId = 'custom',
|
||||||
): void {
|
): void {
|
||||||
|
const defaults = createDefaultChartCustomOverlaySelection();
|
||||||
this._customOverlaySlots[slot].selection = {
|
this._customOverlaySlots[slot].selection = {
|
||||||
smaPlots: { ...selection.smaPlots },
|
smaPlots: { ...defaults.smaPlots, ...selection.smaPlots },
|
||||||
bollinger: { ...selection.bollinger },
|
bollinger: { ...defaults.bollinger, ...selection.bollinger },
|
||||||
ichimoku: { ...selection.ichimoku },
|
ichimoku: { ...defaults.ichimoku, ...selection.ichimoku },
|
||||||
candle: selection.candle,
|
candle: selection.candle,
|
||||||
};
|
};
|
||||||
if (this._customOverlaySlots[slot].active) {
|
if (this._customOverlaySlots[slot].active) {
|
||||||
this.syncChartOverlayVisibility();
|
this._syncCustomOverlayVisibilityToChart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2477,7 +2485,18 @@ export class ChartManager {
|
|||||||
const other: ChartCustomOverlaySlotId = slot === 'custom' ? 'custom1' : 'custom';
|
const other: ChartCustomOverlaySlotId = slot === 'custom' ? 'custom1' : 'custom';
|
||||||
this._customOverlaySlots[other].active = false;
|
this._customOverlaySlots[other].active = false;
|
||||||
}
|
}
|
||||||
|
this._syncCustomOverlayVisibilityToChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Custom 적용 중 — plot·캔들·구름 가시성을 선택값과 동기화 */
|
||||||
|
private _syncCustomOverlayVisibilityToChart(): void {
|
||||||
this.syncChartOverlayVisibility();
|
this.syncChartOverlayVisibility();
|
||||||
|
if (!this._isCustomOverlayApplied()) return;
|
||||||
|
for (const entry of this.indicators.values()) {
|
||||||
|
const config = entry.config;
|
||||||
|
if (!config || !isCustomOverlayPlotControlledType(config.type)) continue;
|
||||||
|
this.applyIndicatorStyle(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** custom 미적용 시 지표 설정 plotVisibility, 적용 시 custom 선택만 반영 */
|
/** custom 미적용 시 지표 설정 plotVisibility, 적용 시 custom 선택만 반영 */
|
||||||
@@ -2496,10 +2515,12 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _resolveCandleVisible(): boolean {
|
private _resolveCandleVisible(): boolean {
|
||||||
if (!this._chartOverlayVisibility.candle) return false;
|
if (this._isCustomOverlayApplied()) {
|
||||||
if (!this._isCustomOverlayApplied()) return true;
|
|
||||||
return this._getAppliedCustomOverlaySelection().candle;
|
return this._getAppliedCustomOverlaySelection().candle;
|
||||||
}
|
}
|
||||||
|
if (!this._chartOverlayVisibility.candle) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private _shouldShowBbBandFillForConfig(config: IndicatorConfig): boolean {
|
private _shouldShowBbBandFillForConfig(config: IndicatorConfig): boolean {
|
||||||
if (!this._isIndicatorEffectivelyVisible(config)) return false;
|
if (!this._isIndicatorEffectivelyVisible(config)) return false;
|
||||||
|
|||||||
@@ -1,4 +1,41 @@
|
|||||||
import type { Timeframe } from '../types';
|
import type { ChartType, Timeframe } from '../types';
|
||||||
|
|
||||||
|
export function toUpbitMarket(symbol: string): string {
|
||||||
|
return symbol.startsWith('KRW-') ? symbol : `KRW-${symbol}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMEFRAME_KO: Record<Timeframe, string> = {
|
||||||
|
'1m': '1분',
|
||||||
|
'3m': '3분',
|
||||||
|
'5m': '5분',
|
||||||
|
'10m': '10분',
|
||||||
|
'15m': '15분',
|
||||||
|
'30m': '30분',
|
||||||
|
'1h': '1시간',
|
||||||
|
'4h': '4시간',
|
||||||
|
'1D': '1일',
|
||||||
|
'1W': '1주',
|
||||||
|
'1M': '1월',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 차트·목록용 시간봉 한글 라벨 */
|
||||||
|
export function formatTimeframeKo(raw: string | undefined): string {
|
||||||
|
const tf = normalizeChartTimeframe(raw);
|
||||||
|
return TIMEFRAME_KO[tf] ?? tf;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHART_TYPE_KO: Record<ChartType, string> = {
|
||||||
|
candlestick: '캔들스틱',
|
||||||
|
bar: '바',
|
||||||
|
line: '라인',
|
||||||
|
area: '영역',
|
||||||
|
baseline: '기준선',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatChartTypeKo(type: ChartType | string | undefined): string {
|
||||||
|
if (!type) return '캔들스틱';
|
||||||
|
return CHART_TYPE_KO[type as ChartType] ?? String(type);
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeChartTimeframe(raw: string | undefined): Timeframe {
|
export function normalizeChartTimeframe(raw: string | undefined): Timeframe {
|
||||||
const t = (raw ?? '1h').trim();
|
const t = (raw ?? '1h').trim();
|
||||||
|
|||||||
@@ -88,6 +88,17 @@ export const ICHIMOKU_CUSTOM_ROWS: Array<{
|
|||||||
{ key: 'bearishCloud', label: '하락 구름' },
|
{ key: 'bearishCloud', label: '하락 구름' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Custom 적용 시 plot 단위로 표시를 제어하는 지표 type */
|
||||||
|
export const CUSTOM_OVERLAY_PLOT_CONTROLLED_TYPES = [
|
||||||
|
'SMA',
|
||||||
|
'BollingerBands',
|
||||||
|
'IchimokuCloud',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function isCustomOverlayPlotControlledType(type: string): boolean {
|
||||||
|
return (CUSTOM_OVERLAY_PLOT_CONTROLLED_TYPES as readonly string[]).includes(type);
|
||||||
|
}
|
||||||
|
|
||||||
/** 지표 설정 plotVisibility 와 무관 — custom 적용 시 이 맵만 사용 */
|
/** 지표 설정 plotVisibility 와 무관 — custom 적용 시 이 맵만 사용 */
|
||||||
export function isCustomPlotSelected(
|
export function isCustomPlotSelected(
|
||||||
sel: ChartCustomOverlaySelection,
|
sel: ChartCustomOverlaySelection,
|
||||||
|
|||||||
Reference in New Issue
Block a user