벡테스팅 수정
This commit is contained in:
@@ -812,6 +812,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
const mgr = new ChartManager(containerRef.current, theme, {
|
const mgr = new ChartManager(containerRef.current, theme, {
|
||||||
compactPaneLayout: paneLayoutClamp,
|
compactPaneLayout: paneLayoutClamp,
|
||||||
autoSize: !paneLayoutClamp,
|
autoSize: !paneLayoutClamp,
|
||||||
|
volumePaneEnabled: volumeVisibleRef.current,
|
||||||
});
|
});
|
||||||
managerRef.current = mgr;
|
managerRef.current = mgr;
|
||||||
setChartMgr(mgr);
|
setChartMgr(mgr);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import {
|
|||||||
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
||||||
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
|
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
|
||||||
import {
|
import {
|
||||||
coalesceTargetsToDefaultStrategy,
|
freezeTargetsBeforeGlobalStrategyChange,
|
||||||
resolveVirtualTargetStrategyId,
|
resolveVirtualTargetStrategyId,
|
||||||
} from '../utils/virtualTargetStrategy';
|
} from '../utils/virtualTargetStrategy';
|
||||||
import { virtualTargetLimitMessage } from '../utils/virtualTargetLimits';
|
import { virtualTargetLimitMessage } from '../utils/virtualTargetLimits';
|
||||||
@@ -165,12 +165,6 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
useEffect(() => { saveVirtualTargets(targets); }, [targets]);
|
useEffect(() => { saveVirtualTargets(targets); }, [targets]);
|
||||||
useEffect(() => { saveVirtualSession(session); }, [session]);
|
useEffect(() => { saveVirtualSession(session); }, [session]);
|
||||||
|
|
||||||
/** 예전 데이터: 종목 strategyId에 전역 ID가 복사된 경우 → 기본 전략 따름(null) */
|
|
||||||
useEffect(() => {
|
|
||||||
if (session.globalStrategyId == null) return;
|
|
||||||
setTargets(prev => coalesceTargetsToDefaultStrategy(prev, session.globalStrategyId));
|
|
||||||
}, [session.globalStrategyId]);
|
|
||||||
|
|
||||||
useEffect(() => { saveVirtualCardViewMode(viewMode); }, [viewMode]);
|
useEffect(() => { saveVirtualCardViewMode(viewMode); }, [viewMode]);
|
||||||
useEffect(() => { saveVirtualGridPreset(gridPreset); }, [gridPreset]);
|
useEffect(() => { saveVirtualGridPreset(gridPreset); }, [gridPreset]);
|
||||||
|
|
||||||
@@ -299,13 +293,11 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/** 상단 기본전략 — 신규 추가 종목 템플릿만 변경, 기존 종목 전략은 유지 */
|
||||||
const handleGlobalStrategyChange = useCallback((strategyId: number | null) => {
|
const handleGlobalStrategyChange = useCallback((strategyId: number | null) => {
|
||||||
const next = { ...session, globalStrategyId: strategyId };
|
setTargets(prev => freezeTargetsBeforeGlobalStrategyChange(prev, session.globalStrategyId));
|
||||||
setSession(next);
|
setSession(prev => ({ ...prev, globalStrategyId: strategyId }));
|
||||||
if (session.running && strategyId) {
|
}, [session.globalStrategyId]);
|
||||||
void syncVirtualTargetsToBackend(targets, next, true);
|
|
||||||
}
|
|
||||||
}, [session, targets]);
|
|
||||||
|
|
||||||
const handleStart = useCallback(async () => {
|
const handleStart = useCallback(async () => {
|
||||||
if (targets.length === 0) {
|
if (targets.length === 0) {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const VirtualLeftTargetPanel: React.FC<Props> = ({
|
|||||||
...targets,
|
...targets,
|
||||||
{
|
{
|
||||||
market,
|
market,
|
||||||
strategyId: null,
|
strategyId: globalStrategyId,
|
||||||
koreanName: names.koreanName,
|
koreanName: names.koreanName,
|
||||||
englishName: names.englishName,
|
englishName: names.englishName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,14 +33,13 @@ const VirtualStrategyChartPopup: React.FC<Props> = ({
|
|||||||
const chartSeriesPriceLabels = chartDefaults.chartSeriesPriceLabels;
|
const chartSeriesPriceLabels = chartDefaults.chartSeriesPriceLabels;
|
||||||
|
|
||||||
const ko = getKoreanName(market);
|
const ko = getKoreanName(market);
|
||||||
const sym = market.replace(/^KRW-/, '');
|
|
||||||
const stratName = strategy?.name ?? '전략 미지정';
|
const stratName = strategy?.name ?? '전략 미지정';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppPopup
|
<AppPopup
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={sym}
|
title={ko}
|
||||||
titleKo={ko}
|
titleKo={stratName}
|
||||||
badge="CHART"
|
badge="CHART"
|
||||||
width={920}
|
width={920}
|
||||||
maxWidth="96vw"
|
maxWidth="96vw"
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { getKoreanName } from '../../utils/marketNameCache';
|
|
||||||
import type { StrategyDto } from '../../utils/backendApi';
|
import type { StrategyDto } from '../../utils/backendApi';
|
||||||
|
import {
|
||||||
|
resolveVirtualTargetNames,
|
||||||
|
resolveVirtualTargetStrategyDisplayName,
|
||||||
|
} from '../../utils/virtualTargetNames';
|
||||||
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
||||||
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
||||||
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||||
@@ -47,6 +50,9 @@ interface Props {
|
|||||||
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
||||||
chartLiveReceiveHighlight?: boolean;
|
chartLiveReceiveHighlight?: boolean;
|
||||||
ticker?: TickerData;
|
ticker?: TickerData;
|
||||||
|
/** 투자대상 저장 한글명·영문 심볼 (없으면 티커·마켓 캐시) */
|
||||||
|
koreanName?: string;
|
||||||
|
englishName?: string;
|
||||||
/** 알림 목록 등 — 푸터 전략 셀렉트 읽기 전용 라벨 */
|
/** 알림 목록 등 — 푸터 전략 셀렉트 읽기 전용 라벨 */
|
||||||
readOnlyStrategyLabel?: string;
|
readOnlyStrategyLabel?: string;
|
||||||
/** 헤더 우측 레이아웃 (알림 목록 신호 슬롯: inline-quote) */
|
/** 헤더 우측 레이아웃 (알림 목록 신호 슬롯: inline-quote) */
|
||||||
@@ -78,12 +84,22 @@ const VirtualTargetCard: React.FC<Props> = ({
|
|||||||
chartPaneSeparator,
|
chartPaneSeparator,
|
||||||
chartLiveReceiveHighlight = true,
|
chartLiveReceiveHighlight = true,
|
||||||
ticker,
|
ticker,
|
||||||
|
koreanName,
|
||||||
|
englishName,
|
||||||
readOnlyStrategyLabel,
|
readOnlyStrategyLabel,
|
||||||
headVariant = 'default',
|
headVariant = 'default',
|
||||||
}) => {
|
}) => {
|
||||||
const inlineHeadQuote = headVariant === 'inline-quote';
|
const inlineHeadQuote = headVariant === 'inline-quote';
|
||||||
const ko = getKoreanName(market);
|
const { koreanName: displayKo, englishName: displayEn } = resolveVirtualTargetNames(
|
||||||
const sym = market.replace(/^KRW-/, '');
|
market,
|
||||||
|
koreanName ?? ticker?.koreanName,
|
||||||
|
englishName,
|
||||||
|
);
|
||||||
|
const strategyDisplayName = resolveVirtualTargetStrategyDisplayName(
|
||||||
|
strategy,
|
||||||
|
strategies,
|
||||||
|
readOnlyStrategyLabel,
|
||||||
|
);
|
||||||
const rows = snapshot?.rows ?? [];
|
const rows = snapshot?.rows ?? [];
|
||||||
|
|
||||||
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
|
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
|
||||||
@@ -129,9 +145,18 @@ const VirtualTargetCard: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<div className="vtd-card-head">
|
<div className="vtd-card-head">
|
||||||
<div className="vtd-card-head-main">
|
<div className="vtd-card-head-main">
|
||||||
<div className="vtd-card-title">
|
<div className={`vtd-card-title${isChart ? ' vtd-card-title--chart' : ''}`}>
|
||||||
<span className="vtd-card-ko">{ko}</span>
|
{isChart ? (
|
||||||
<span className="vtd-card-sym">{sym}</span>
|
<>
|
||||||
|
<span className="vtd-card-chart-strategy">{strategyDisplayName}</span>
|
||||||
|
<span className="vtd-card-ko">{displayKo}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="vtd-card-ko">{displayKo}</span>
|
||||||
|
<span className="vtd-card-sym">{displayEn}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{inlineHeadQuote ? (
|
{inlineHeadQuote ? (
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
onCandlesReady={handleCandlesReady}
|
onCandlesReady={handleCandlesReady}
|
||||||
magnifierEnabled={false}
|
magnifierEnabled={false}
|
||||||
volumeVisible={false}
|
volumeVisible={false}
|
||||||
|
paneLayoutClamp
|
||||||
showHoverToolbar={false}
|
showHoverToolbar={false}
|
||||||
showPaneLegend={false}
|
showPaneLegend={false}
|
||||||
showCandlePaneControls={false}
|
showCandlePaneControls={false}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { getKoreanName } from '../../utils/marketNameCache';
|
|
||||||
import type { StrategyDto } from '../../utils/backendApi';
|
import type { StrategyDto } from '../../utils/backendApi';
|
||||||
|
import {
|
||||||
|
resolveVirtualTargetNames,
|
||||||
|
resolveVirtualTargetStrategyDisplayName,
|
||||||
|
} from '../../utils/virtualTargetNames';
|
||||||
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
||||||
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
||||||
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||||
@@ -37,6 +40,9 @@ interface Props {
|
|||||||
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
||||||
chartLiveReceiveHighlight?: boolean;
|
chartLiveReceiveHighlight?: boolean;
|
||||||
ticker?: TickerData;
|
ticker?: TickerData;
|
||||||
|
koreanName?: string;
|
||||||
|
englishName?: string;
|
||||||
|
readOnlyStrategyLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 전체보기 — 좌 차트 · 우 신호 */
|
/** 전체보기 — 좌 차트 · 우 신호 */
|
||||||
@@ -61,9 +67,20 @@ const VirtualTargetFocusView: React.FC<Props> = ({
|
|||||||
chartPaneSeparator,
|
chartPaneSeparator,
|
||||||
chartLiveReceiveHighlight = true,
|
chartLiveReceiveHighlight = true,
|
||||||
ticker,
|
ticker,
|
||||||
|
koreanName,
|
||||||
|
englishName,
|
||||||
|
readOnlyStrategyLabel,
|
||||||
}) => {
|
}) => {
|
||||||
const ko = getKoreanName(market);
|
const { koreanName: displayKo } = resolveVirtualTargetNames(
|
||||||
const sym = market.replace(/^KRW-/, '');
|
market,
|
||||||
|
koreanName ?? ticker?.koreanName,
|
||||||
|
englishName,
|
||||||
|
);
|
||||||
|
const strategyDisplayName = resolveVirtualTargetStrategyDisplayName(
|
||||||
|
strategy,
|
||||||
|
strategies,
|
||||||
|
readOnlyStrategyLabel,
|
||||||
|
);
|
||||||
|
|
||||||
const receiveSignal = useMemo(() => {
|
const receiveSignal = useMemo(() => {
|
||||||
if (!running) return null;
|
if (!running) return null;
|
||||||
@@ -80,9 +97,9 @@ const VirtualTargetFocusView: React.FC<Props> = ({
|
|||||||
<div className={`vtd-focus-wrap${highlightReceiving ? ' vtd-focus-wrap--receiving' : ''}`}>
|
<div className={`vtd-focus-wrap${highlightReceiving ? ' vtd-focus-wrap--receiving' : ''}`}>
|
||||||
<div className="vtd-focus-head">
|
<div className="vtd-focus-head">
|
||||||
<div className="vtd-focus-head-main">
|
<div className="vtd-focus-head-main">
|
||||||
<div className="vtd-focus-title">
|
<div className="vtd-focus-title vtd-card-title--chart">
|
||||||
<span className="vtd-card-ko">{ko}</span>
|
<span className="vtd-card-chart-strategy">{strategyDisplayName}</span>
|
||||||
<span className="vtd-card-sym">{sym}</span>
|
<span className="vtd-card-ko">{displayKo}</span>
|
||||||
</div>
|
</div>
|
||||||
{running && <VirtualLiveBadge status={liveStatus} receiving={receiving} />}
|
{running && <VirtualLiveBadge status={liveStatus} receiving={receiving} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
|||||||
chartPaneSeparator={chartPaneSeparator}
|
chartPaneSeparator={chartPaneSeparator}
|
||||||
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
||||||
ticker={tickers?.get(focusTarget.market)}
|
ticker={tickers?.get(focusTarget.market)}
|
||||||
|
koreanName={focusTarget.koreanName}
|
||||||
|
englishName={focusTarget.englishName}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@@ -180,6 +182,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
|||||||
chartPaneSeparator={chartPaneSeparator}
|
chartPaneSeparator={chartPaneSeparator}
|
||||||
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
||||||
ticker={tickers?.get(t.market)}
|
ticker={tickers?.get(t.market)}
|
||||||
|
koreanName={t.koreanName}
|
||||||
|
englishName={t.englishName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
||||||
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
|
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
|
||||||
import {
|
import {
|
||||||
coalesceTargetsToDefaultStrategy,
|
freezeTargetsBeforeGlobalStrategyChange,
|
||||||
resolveVirtualTargetStrategyId,
|
resolveVirtualTargetStrategyId,
|
||||||
} from '../utils/virtualTargetStrategy';
|
} from '../utils/virtualTargetStrategy';
|
||||||
import {
|
import {
|
||||||
@@ -186,11 +186,6 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
|
|||||||
}, [session]);
|
}, [session]);
|
||||||
useEffect(() => { saveVirtualCardViewMode(viewMode); }, [viewMode]);
|
useEffect(() => { saveVirtualCardViewMode(viewMode); }, [viewMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (session.globalStrategyId == null) return;
|
|
||||||
setTargets(prev => coalesceTargetsToDefaultStrategy(prev, session.globalStrategyId));
|
|
||||||
}, [session.globalStrategyId]);
|
|
||||||
|
|
||||||
const strategyNames = useMemo(
|
const strategyNames = useMemo(
|
||||||
() => Object.fromEntries(strategies.map(s => [s.id, s.name])),
|
() => Object.fromEntries(strategies.map(s => [s.id, s.name])),
|
||||||
[strategies],
|
[strategies],
|
||||||
@@ -258,13 +253,13 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
|
|||||||
const names = resolveVirtualTargetNames(market, meta?.koreanName, meta?.englishName);
|
const names = resolveVirtualTargetNames(market, meta?.koreanName, meta?.englishName);
|
||||||
setTargets(prev => [...prev, {
|
setTargets(prev => [...prev, {
|
||||||
market,
|
market,
|
||||||
strategyId: null,
|
strategyId: session.globalStrategyId,
|
||||||
koreanName: names.koreanName,
|
koreanName: names.koreanName,
|
||||||
englishName: names.englishName,
|
englishName: names.englishName,
|
||||||
}]);
|
}]);
|
||||||
setSelectedMarket(market);
|
setSelectedMarket(market);
|
||||||
return true;
|
return true;
|
||||||
}, [targets, virtualTargetMaxCount]);
|
}, [targets, session.globalStrategyId, virtualTargetMaxCount]);
|
||||||
|
|
||||||
const handleRemoveTarget = useCallback((market: string) => {
|
const handleRemoveTarget = useCallback((market: string) => {
|
||||||
setTargets(prev => prev.filter(x => x.market !== market));
|
setTargets(prev => prev.filter(x => x.market !== market));
|
||||||
@@ -282,13 +277,11 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/** 상단 기본전략 — 신규 추가 종목 템플릿만 변경, 기존 종목 전략은 유지 */
|
||||||
const handleGlobalStrategyChange = useCallback((strategyId: number | null) => {
|
const handleGlobalStrategyChange = useCallback((strategyId: number | null) => {
|
||||||
const next = { ...session, globalStrategyId: strategyId };
|
setTargets(prev => freezeTargetsBeforeGlobalStrategyChange(prev, session.globalStrategyId));
|
||||||
setSession(next);
|
setSession(prev => ({ ...prev, globalStrategyId: strategyId }));
|
||||||
if (session.running && strategyId) {
|
}, [session.globalStrategyId]);
|
||||||
void syncVirtualTargetsToBackend(targets, next, true);
|
|
||||||
}
|
|
||||||
}, [session, targets]);
|
|
||||||
|
|
||||||
const handleStart = useCallback(async () => {
|
const handleStart = useCallback(async () => {
|
||||||
if (targets.length === 0) {
|
if (targets.length === 0) {
|
||||||
|
|||||||
@@ -1662,6 +1662,19 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vtd-card-title--chart {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vtd-card-chart-strategy {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--se-text-muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.vtd-card-ko {
|
.vtd-card-ko {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
@@ -307,6 +307,8 @@ export class ChartManager {
|
|||||||
|
|
||||||
/** 차트 하단 거래량 pane 표시 */
|
/** 차트 하단 거래량 pane 표시 */
|
||||||
private _volumeVisible = true;
|
private _volumeVisible = true;
|
||||||
|
/** false — 거래량 시리즈·pane 1 미생성 (가상매매 카드 등) */
|
||||||
|
private _volumePaneEnabled = true;
|
||||||
|
|
||||||
/** pane 간 구분선 (설정 화면) */
|
/** pane 간 구분선 (설정 화면) */
|
||||||
private _paneSeparatorOptions: ChartPaneSeparatorOptions =
|
private _paneSeparatorOptions: ChartPaneSeparatorOptions =
|
||||||
@@ -324,10 +326,12 @@ export class ChartManager {
|
|||||||
constructor(
|
constructor(
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
options?: { autoSize?: boolean; compactPaneLayout?: boolean },
|
options?: { autoSize?: boolean; compactPaneLayout?: boolean; volumePaneEnabled?: boolean },
|
||||||
) {
|
) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this._compactPaneLayout = options?.compactPaneLayout ?? false;
|
this._compactPaneLayout = options?.compactPaneLayout ?? false;
|
||||||
|
this._volumePaneEnabled = options?.volumePaneEnabled ?? true;
|
||||||
|
if (!this._volumePaneEnabled) this._volumeVisible = false;
|
||||||
const autoSize = options?.autoSize ?? !this._compactPaneLayout;
|
const autoSize = options?.autoSize ?? !this._compactPaneLayout;
|
||||||
const t = getTheme(theme);
|
const t = getTheme(theme);
|
||||||
this.chart = createChart(container, {
|
this.chart = createChart(container, {
|
||||||
@@ -454,6 +458,8 @@ export class ChartManager {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(this.mainSeries as ISeriesApi<any>).setData(mainData);
|
(this.mainSeries as ISeriesApi<any>).setData(mainData);
|
||||||
|
|
||||||
|
const panesInit = this.chart.panes();
|
||||||
|
if (this._volumePaneEnabled) {
|
||||||
// Volume sub-pane (pane index 1)
|
// Volume sub-pane (pane index 1)
|
||||||
this.volumeSeries = this.chart.addSeries(HistogramSeries, {
|
this.volumeSeries = this.chart.addSeries(HistogramSeries, {
|
||||||
priceFormat: { type: 'volume' },
|
priceFormat: { type: 'volume' },
|
||||||
@@ -461,8 +467,6 @@ export class ChartManager {
|
|||||||
}, 1);
|
}, 1);
|
||||||
|
|
||||||
// 초기 volume pane 크기: resetPaneHeights() 호출 전 임시 설정
|
// 초기 volume pane 크기: resetPaneHeights() 호출 전 임시 설정
|
||||||
// (setHeight 대신 setStretchFactor - LWC 내부 기준 높이와 무관하게 동작)
|
|
||||||
const panesInit = this.chart.panes();
|
|
||||||
if (panesInit[0]) panesInit[0].setStretchFactor(0.88);
|
if (panesInit[0]) panesInit[0].setStretchFactor(0.88);
|
||||||
if (panesInit[1]) panesInit[1].setStretchFactor(0.12);
|
if (panesInit[1]) panesInit[1].setStretchFactor(0.12);
|
||||||
|
|
||||||
@@ -477,6 +481,9 @@ export class ChartManager {
|
|||||||
this.volumeSeries.applyOptions({ visible: false } as Parameters<ISeriesApi<SeriesType>['applyOptions']>[0]);
|
this.volumeSeries.applyOptions({ visible: false } as Parameters<ISeriesApi<SeriesType>['applyOptions']>[0]);
|
||||||
} catch { /* ok */ }
|
} catch { /* ok */ }
|
||||||
}
|
}
|
||||||
|
} else if (panesInit[0]) {
|
||||||
|
panesInit[0].setStretchFactor(1);
|
||||||
|
}
|
||||||
|
|
||||||
this._reapplyAllPatternMarkers();
|
this._reapplyAllPatternMarkers();
|
||||||
|
|
||||||
@@ -2621,6 +2628,10 @@ export class ChartManager {
|
|||||||
|
|
||||||
/** 차트 설정: 거래량 pane 표시 on/off */
|
/** 차트 설정: 거래량 pane 표시 on/off */
|
||||||
setVolumeVisible(visible: boolean, availableHeight?: number): void {
|
setVolumeVisible(visible: boolean, availableHeight?: number): void {
|
||||||
|
if (!this._volumePaneEnabled) {
|
||||||
|
this._volumeVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._volumeVisible = visible;
|
this._volumeVisible = visible;
|
||||||
if (this._candleOnlyLayout) return;
|
if (this._candleOnlyLayout) return;
|
||||||
this.applyVolumeVisibility(visible, availableHeight);
|
this.applyVolumeVisibility(visible, availableHeight);
|
||||||
@@ -3954,7 +3965,7 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _volumeFrac(H: number): number {
|
private _volumeFrac(H: number): number {
|
||||||
if (!this._volumeVisible || this._candleOnlyLayout) return 0;
|
if (!this._volumePaneEnabled || !this._volumeVisible || this._candleOnlyLayout) return 0;
|
||||||
return Math.min(Math.max(60 / H, 0.04), 0.12);
|
return Math.min(Math.max(60 / H, 0.04), 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3970,7 +3981,7 @@ export class ChartManager {
|
|||||||
? this._lastLayoutAvailableHeight
|
? this._lastLayoutAvailableHeight
|
||||||
: this.container.clientHeight);
|
: this.container.clientHeight);
|
||||||
|
|
||||||
const volumeShown = this._volumeVisible && !this._candleOnlyLayout;
|
const volumeShown = this._volumePaneEnabled && this._volumeVisible && !this._candleOnlyLayout;
|
||||||
const VOL_FRAC = this._volumeFrac(H);
|
const VOL_FRAC = this._volumeFrac(H);
|
||||||
const MIN_IND_PX = this._minIndPx(H);
|
const MIN_IND_PX = this._minIndPx(H);
|
||||||
const ORPHAN_STRETCH = 0.0001;
|
const ORPHAN_STRETCH = 0.0001;
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export async function addVirtualTarget(
|
|||||||
const en = meta.englishName ?? market.replace(/^KRW-/, '');
|
const en = meta.englishName ?? market.replace(/^KRW-/, '');
|
||||||
const item: VirtualTargetItem = {
|
const item: VirtualTargetItem = {
|
||||||
market,
|
market,
|
||||||
/** null = 상단 기본 전략 사용 */
|
/** 신규 추가 시점의 상단 기본전략(템플릿)을 종목에 고정 */
|
||||||
strategyId: null,
|
strategyId: session.globalStrategyId,
|
||||||
koreanName: meta.koreanName,
|
koreanName: meta.koreanName,
|
||||||
englishName: en,
|
englishName: en,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,3 +33,15 @@ export function formatVirtualTargetOptionLabel(
|
|||||||
const { koreanName: ko, englishName: en } = resolveVirtualTargetNames(market, koreanName, englishName);
|
const { koreanName: ko, englishName: en } = resolveVirtualTargetNames(market, koreanName, englishName);
|
||||||
return `${ko}(${en})`;
|
return `${ko}(${en})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 카드·차트 상단 — 투자전략 표시명 */
|
||||||
|
export function resolveVirtualTargetStrategyDisplayName(
|
||||||
|
strategy: { name?: string } | undefined,
|
||||||
|
strategies: Array<{ id?: number; name?: string }>,
|
||||||
|
readOnlyStrategyLabel?: string,
|
||||||
|
): string {
|
||||||
|
const ro = readOnlyStrategyLabel?.trim();
|
||||||
|
if (ro) return ro;
|
||||||
|
if (strategy?.name?.trim()) return strategy.name.trim();
|
||||||
|
return '전략 미지정';
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import type { VirtualTargetItem } from './virtualTradingStorage';
|
|||||||
/** 종목 전략 셀렉트 — 기본(전역) 전략 옵션 값 */
|
/** 종목 전략 셀렉트 — 기본(전역) 전략 옵션 값 */
|
||||||
export const VIRTUAL_DEFAULT_STRATEGY_VALUE = '__default__';
|
export const VIRTUAL_DEFAULT_STRATEGY_VALUE = '__default__';
|
||||||
|
|
||||||
/** 종목에 전략이 지정되지 않았으면 전역(기본) 전략 ID */
|
/**
|
||||||
|
* 종목별 실효 전략 ID.
|
||||||
|
* strategyId 가 있으면 그대로 사용하고, null 이면 globalStrategyId(레거시·미설정)만 참조.
|
||||||
|
* 상단 기본전략 변경은 기존 null 종목을 freezeTargetsBeforeGlobalStrategyChange 로 고정한 뒤 session 만 갱신.
|
||||||
|
*/
|
||||||
export function resolveVirtualTargetStrategyId(
|
export function resolveVirtualTargetStrategyId(
|
||||||
target: Pick<VirtualTargetItem, 'strategyId'>,
|
target: Pick<VirtualTargetItem, 'strategyId'>,
|
||||||
globalStrategyId: number | null,
|
globalStrategyId: number | null,
|
||||||
@@ -12,6 +16,21 @@ export function resolveVirtualTargetStrategyId(
|
|||||||
return globalStrategyId;
|
return globalStrategyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 기본전략 변경 직전 — null(기본 따름) 종목을 이전 기본전략 ID 로 고정 */
|
||||||
|
export function freezeTargetsBeforeGlobalStrategyChange(
|
||||||
|
targets: VirtualTargetItem[],
|
||||||
|
previousGlobalStrategyId: number | null,
|
||||||
|
): VirtualTargetItem[] {
|
||||||
|
if (previousGlobalStrategyId == null) return targets;
|
||||||
|
let changed = false;
|
||||||
|
const next = targets.map(t => {
|
||||||
|
if (t.strategyId != null) return t;
|
||||||
|
changed = true;
|
||||||
|
return { ...t, strategyId: previousGlobalStrategyId };
|
||||||
|
});
|
||||||
|
return changed ? next : targets;
|
||||||
|
}
|
||||||
|
|
||||||
export function usesDefaultStrategy(target: Pick<VirtualTargetItem, 'strategyId'>): boolean {
|
export function usesDefaultStrategy(target: Pick<VirtualTargetItem, 'strategyId'>): boolean {
|
||||||
return target.strategyId == null;
|
return target.strategyId == null;
|
||||||
}
|
}
|
||||||
@@ -26,6 +45,7 @@ export function parseTargetStrategySelectValue(value: string): number | null {
|
|||||||
return Number.isFinite(n) ? n : null;
|
return Number.isFinite(n) ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 종목 셀렉트 — 기본 전략(전역) 옵션 라벨 (신규 추가 시 적용되는 템플릿) */
|
||||||
export function defaultStrategyOptionLabel(
|
export function defaultStrategyOptionLabel(
|
||||||
globalStrategyId: number | null,
|
globalStrategyId: number | null,
|
||||||
strategies: Array<{ id?: number; name?: string }>,
|
strategies: Array<{ id?: number; name?: string }>,
|
||||||
@@ -35,21 +55,3 @@ export function defaultStrategyOptionLabel(
|
|||||||
return name ? `기본 전략 (${name})` : '기본 전략';
|
return name ? `기본 전략 (${name})` : '기본 전략';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 예전에 전역 전략 ID가 종목에 복사 저장된 항목 → null(기본 전략 따름)로 정규화
|
|
||||||
*/
|
|
||||||
export function coalesceTargetsToDefaultStrategy(
|
|
||||||
targets: VirtualTargetItem[],
|
|
||||||
globalStrategyId: number | null,
|
|
||||||
): VirtualTargetItem[] {
|
|
||||||
if (globalStrategyId == null) return targets;
|
|
||||||
let changed = false;
|
|
||||||
const next = targets.map(t => {
|
|
||||||
if (t.strategyId != null && t.strategyId === globalStrategyId) {
|
|
||||||
changed = true;
|
|
||||||
return { ...t, strategyId: null };
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
});
|
|
||||||
return changed ? next : targets;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user