라벨 중복문제 수정
This commit is contained in:
+29
-9
@@ -9295,8 +9295,7 @@ html.theme-blue {
|
|||||||
.rsp-trade-stack .rsp-trade-card-body {
|
.rsp-trade-stack .rsp-trade-card-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
container-type: size;
|
container-type: size;
|
||||||
@@ -9332,14 +9331,25 @@ html.theme-blue {
|
|||||||
color: var(--down);
|
color: var(--down);
|
||||||
}
|
}
|
||||||
.rsp-trade-card-body .top-panel {
|
.rsp-trade-card-body .top-panel {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 6px 10px 6px;
|
||||||
|
}
|
||||||
|
.rsp-trade-card-body .top-panel-fields {
|
||||||
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-evenly;
|
||||||
gap: 6px;
|
gap: clamp(4px, 1.2cqi, 10px);
|
||||||
background: transparent;
|
overflow-y: auto;
|
||||||
padding: 6px 10px 6px;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
.rsp-trade-card-body .top-field {
|
.rsp-trade-card-body .top-field {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -9382,7 +9392,9 @@ html.theme-blue {
|
|||||||
}
|
}
|
||||||
.rsp-trade-card-body .top-actions {
|
.rsp-trade-card-body .top-actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: auto;
|
margin-top: clamp(6px, 1.5cqi, 12px);
|
||||||
|
padding-top: clamp(4px, 1cqi, 8px);
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
|
||||||
}
|
}
|
||||||
.rsp-trade-card-body .top-reset,
|
.rsp-trade-card-body .top-reset,
|
||||||
.rsp-trade-card-body .top-submit {
|
.rsp-trade-card-body .top-submit {
|
||||||
@@ -9390,7 +9402,15 @@ html.theme-blue {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 가격·수량: 기본 세로 배치, 공간 부족 시 한 줄 */
|
.rsp-trade-card-body .top-price-qty-block {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
gap: clamp(4px, 1cqi, 8px);
|
||||||
|
}
|
||||||
|
/* 가격·수량: 기본 세로 배치 */
|
||||||
.top-price-qty-block {
|
.top-price-qty-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`top-panel top-panel--${side}`}>
|
<div className={`top-panel top-panel--${side}`}>
|
||||||
|
<div className="top-panel-fields">
|
||||||
{paperTradingEnabled && !paperAutoTradeEnabled && showSymbolField && (
|
{paperTradingEnabled && !paperAutoTradeEnabled && showSymbolField && (
|
||||||
<p className="top-paper-hint">자동매매 OFF · 수동 주문만 모의 계좌에 체결됩니다.</p>
|
<p className="top-paper-hint">자동매매 OFF · 수동 주문만 모의 계좌에 체결됩니다.</p>
|
||||||
)}
|
)}
|
||||||
@@ -399,6 +400,7 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
|||||||
<span>수수료 {feeRate}%</span>
|
<span>수수료 {feeRate}%</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="top-actions">
|
<div className="top-actions">
|
||||||
<button type="button" className="top-reset" onClick={handleReset}>
|
<button type="button" className="top-reset" onClick={handleReset}>
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
}, [market, barsMarket]);
|
}, [market, barsMarket]);
|
||||||
|
|
||||||
const queueIndicatorRecovery = useCallback(() => {
|
const queueIndicatorRecovery = useCallback(() => {
|
||||||
|
if (reloadInFlightRef.current) return;
|
||||||
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
|
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
|
||||||
recoveryTimerRef.current = window.setTimeout(() => {
|
recoveryTimerRef.current = window.setTimeout(() => {
|
||||||
recoveryTimerRef.current = null;
|
recoveryTimerRef.current = null;
|
||||||
@@ -889,7 +890,13 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
|
|
||||||
if (!barsChanged && !indChanged && !ctChanged && !thChanged && !lsChanged && !indicatorsIncomplete) return;
|
if (!barsChanged && !indChanged && !ctChanged && !thChanged && !lsChanged && !indicatorsIncomplete) return;
|
||||||
|
|
||||||
if (barsChanged || ctChanged || thChanged || indicatorsIncomplete) {
|
if (thChanged && !barsChanged && !indChanged && !ctChanged && !lsChanged && !indicatorsIncomplete) {
|
||||||
|
prevTheme.current = theme;
|
||||||
|
mgr.setTheme(theme);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barsChanged || ctChanged || indicatorsIncomplete) {
|
||||||
// 데이터·차트 형식 변경 또는 지표 누락 → 전체 재로드
|
// 데이터·차트 형식 변경 또는 지표 누락 → 전체 재로드
|
||||||
void reloadAll(mgr, bars, chartType, theme, logScale, indicators);
|
void reloadAll(mgr, bars, chartType, theme, logScale, indicators);
|
||||||
} else if (lsChanged) {
|
} else if (lsChanged) {
|
||||||
@@ -1011,6 +1018,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
|
|
||||||
const timers = [120, 400, 900].map(delay =>
|
const timers = [120, 400, 900].map(delay =>
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
|
if (reloadInFlightRef.current) return;
|
||||||
const m = managerRef.current;
|
const m = managerRef.current;
|
||||||
if (!m || !m.hasMainSeries()) return;
|
if (!m || !m.hasMainSeries()) return;
|
||||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||||
|
|||||||
@@ -122,21 +122,36 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
|||||||
}, [market, chartTf]);
|
}, [market, chartTf]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const wrap = canvasWrapRef.current;
|
||||||
|
if (!wrap) return;
|
||||||
|
|
||||||
|
const applyPaneHeights = () => {
|
||||||
|
const h = wrap.clientHeight;
|
||||||
|
if (h <= 0) return;
|
||||||
|
const mgr = managerRef.current;
|
||||||
|
if (mgr?.hasMainSeries()) mgr.resetPaneHeights(h);
|
||||||
|
};
|
||||||
|
|
||||||
const timers = [100, 300, 800, 1500].map(delay =>
|
const timers = [100, 300, 800, 1500].map(delay =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const wrap = canvasWrapRef.current;
|
if (!canvasWrapRef.current) return;
|
||||||
const mgr = managerRef.current;
|
const h = canvasWrapRef.current.clientHeight;
|
||||||
if (!wrap) return;
|
|
||||||
const h = wrap.clientHeight;
|
|
||||||
if (h <= 0) return;
|
if (h <= 0) return;
|
||||||
if (mgr?.hasMainSeries()) mgr.resetPaneHeights(h);
|
if (managerRef.current?.hasMainSeries()) managerRef.current.resetPaneHeights(h);
|
||||||
else if (!chartReloadTriggeredRef.current && delay >= 800 && bars.length > 0) {
|
else if (!chartReloadTriggeredRef.current && delay >= 800 && bars.length > 0) {
|
||||||
chartReloadTriggeredRef.current = true;
|
chartReloadTriggeredRef.current = true;
|
||||||
setChartReloadTick(t => t + 1);
|
setChartReloadTick(t => t + 1);
|
||||||
}
|
}
|
||||||
}, delay),
|
}, delay),
|
||||||
);
|
);
|
||||||
return () => timers.forEach(clearTimeout);
|
|
||||||
|
const ro = new ResizeObserver(() => applyPaneHeights());
|
||||||
|
ro.observe(wrap);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
timers.forEach(clearTimeout);
|
||||||
|
ro.disconnect();
|
||||||
|
};
|
||||||
}, [market, chartTf, bars.length]);
|
}, [market, chartTf, bars.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -145,8 +160,11 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
|||||||
}, [market, chartTf, useUpbit]);
|
}, [market, chartTf, useUpbit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vtd-card-chart-wrap">
|
<div className="vtd-card-chart-panel vtd-card-chart-panel--fill">
|
||||||
<div ref={canvasWrapRef} className="vtd-card-chart slot-chart-wrap">
|
<div
|
||||||
|
ref={canvasWrapRef}
|
||||||
|
className="vtd-card-chart-canvas slot-chart-wrap vtd-card-chart-canvas--fill"
|
||||||
|
>
|
||||||
{isLoading && <div className="vtd-card-chart-loading">차트 로딩…</div>}
|
{isLoading && <div className="vtd-card-chart-loading">차트 로딩…</div>}
|
||||||
{isLoadingMore && (
|
{isLoadingMore && (
|
||||||
<div className="chart-history-loading">
|
<div className="chart-history-loading">
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
|||||||
className={[
|
className={[
|
||||||
'vtd-card',
|
'vtd-card',
|
||||||
'vtd-card--signal',
|
'vtd-card--signal',
|
||||||
'vtd-card--detail',
|
!isChart ? 'vtd-card--detail' : '',
|
||||||
isChart ? 'vtd-card--chart-mode' : '',
|
isChart ? 'vtd-card--chart-mode tsd-result-card--chart' : '',
|
||||||
selected ? 'vtd-card--selected' : '',
|
selected ? 'vtd-card--selected' : '',
|
||||||
flashCls,
|
flashCls,
|
||||||
].filter(Boolean).join(' ')}
|
].filter(Boolean).join(' ')}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { StrategyDto } from '../../utils/backendApi';
|
|||||||
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';
|
||||||
import { buildConditionMetrics, resolveVirtualTradeTiming } from '../../utils/virtualSignalMetrics';
|
import { buildConditionMetrics } from '../../utils/virtualSignalMetrics';
|
||||||
import { candleTypeToTimeframe } from '../../utils/strategyToChartIndicators';
|
import { candleTypeToTimeframe } from '../../utils/strategyToChartIndicators';
|
||||||
import { normalizeStartCandleType } from '../../utils/strategyStartNodes';
|
import { normalizeStartCandleType } from '../../utils/strategyStartNodes';
|
||||||
import type { Timeframe } from '../../types';
|
import type { Timeframe } from '../../types';
|
||||||
@@ -76,7 +76,6 @@ const VirtualTargetCard: React.FC<Props> = ({
|
|||||||
const rows = snapshot?.rows ?? [];
|
const rows = snapshot?.rows ?? [];
|
||||||
|
|
||||||
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
|
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
|
||||||
const tradeTiming = useMemo(() => resolveVirtualTradeTiming(metrics), [metrics]);
|
|
||||||
const metCount = metrics.filter(m => m.status === 'match').length;
|
const metCount = metrics.filter(m => m.status === 'match').length;
|
||||||
const evalCount = metrics.filter(m => m.status !== 'unknown').length;
|
const evalCount = metrics.filter(m => m.status !== 'unknown').length;
|
||||||
|
|
||||||
@@ -102,8 +101,6 @@ const VirtualTargetCard: React.FC<Props> = ({
|
|||||||
isDetail ? 'vtd-card--detail' : 'vtd-card--summary',
|
isDetail ? 'vtd-card--detail' : 'vtd-card--summary',
|
||||||
isChart ? 'vtd-card--chart-mode' : '',
|
isChart ? 'vtd-card--chart-mode' : '',
|
||||||
selected ? 'vtd-card--selected' : '',
|
selected ? 'vtd-card--selected' : '',
|
||||||
tradeTiming === 'buy' ? 'vtd-card--timing-buy' : '',
|
|
||||||
tradeTiming === 'sell' ? 'vtd-card--timing-sell' : '',
|
|
||||||
].filter(Boolean).join(' ')}
|
].filter(Boolean).join(' ')}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
|
|||||||
@@ -780,6 +780,49 @@
|
|||||||
|
|
||||||
.bps-page--tsd .vtd-grid--chart-mode {
|
.bps-page--tsd .vtd-grid--chart-mode {
|
||||||
grid-auto-rows: minmax(560px, auto);
|
grid-auto-rows: minmax(560px, auto);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 추세검색 카드 — 차트 모드: 헤더·메타 제외 영역을 차트가 채움 */
|
||||||
|
.bps-page--tsd .tsd-result-card--chart.vtd-card--chart-mode {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 560px;
|
||||||
|
overflow: hidden;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--tsd .tsd-result-card--chart .vtd-card-head,
|
||||||
|
.bps-page--tsd .tsd-result-card--chart .vtd-card-meta {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--tsd .tsd-result-card--chart .vtd-card-chart-panel--fill {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--tsd .tsd-result-card--chart .vtd-card-chart-canvas--fill {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: auto !important;
|
||||||
|
border: 1px solid var(--se-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--se-center-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--tsd .tsd-result-card--chart .vtd-card-chart-canvas--fill > .tv-chart-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--tsd .tsd-result-card--chart .vtd-card-chart-canvas--fill .chart-hover-toolbar {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tsd-card-high-badge {
|
.tsd-card-high-badge {
|
||||||
|
|||||||
@@ -1020,33 +1020,33 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vtd-card[role='button']:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vtd-card[role='button']:focus-visible {
|
||||||
|
outline: 1px solid color-mix(in srgb, var(--se-gold) 55%, var(--se-border));
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 선택 카드 — 기본 카드와 동일한 1px 테두리 (굵은 파란 outline 제거) */
|
||||||
.vtd-card--selected {
|
.vtd-card--selected {
|
||||||
outline: 2px solid var(--accent, #3f7ef5);
|
border-color: color-mix(in srgb, var(--se-gold) 35%, var(--se-border));
|
||||||
outline-offset: 2px;
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TradeAlertModal: 매수 #3f7ef5, 매도 #ef5350 */
|
/* 매수·매도 타이밍 — 카드 테두리 강조 비활성 (신호등·패널에서만 표시) */
|
||||||
.vtd-card--timing-buy {
|
.vtd-card--timing-buy,
|
||||||
border-color: #3f7ef5 !important;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 1px rgba(63, 126, 245, 0.45),
|
|
||||||
0 0 16px rgba(63, 126, 245, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vtd-card--timing-sell {
|
.vtd-card--timing-sell {
|
||||||
border-color: #ef5350 !important;
|
border-color: color-mix(in srgb, var(--se-gold) 35%, var(--se-border));
|
||||||
box-shadow:
|
box-shadow: none;
|
||||||
0 0 0 1px rgba(239, 83, 80, 0.45),
|
|
||||||
0 0 16px rgba(239, 83, 80, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vtd-card--selected.vtd-card--timing-buy {
|
|
||||||
outline-color: #3f7ef5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vtd-card--selected.vtd-card--timing-buy,
|
||||||
.vtd-card--selected.vtd-card--timing-sell {
|
.vtd-card--selected.vtd-card--timing-sell {
|
||||||
outline-color: #ef5350;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vtd-card--live {
|
.vtd-card--live {
|
||||||
@@ -2402,3 +2402,189 @@
|
|||||||
background: #787b86;
|
background: #787b86;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 우측 매매 패널: 매수·매도 카드 간격·타이포 ── */
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-split-card-body {
|
||||||
|
padding: 8px 12px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card > .top-panel {
|
||||||
|
gap: 0;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 6px 6px 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-panel-fields {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-field,
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-row--balance {
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 기본: 공간 부족 시 하단 안내 텍스트 숨김 */
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-meta {
|
||||||
|
display: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-label {
|
||||||
|
margin-bottom: 7px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-input,
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-input--full {
|
||||||
|
padding: 9px 11px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-symbol-input {
|
||||||
|
padding: 9px 11px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-order-kind {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-kind-btn {
|
||||||
|
padding: 7px 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-row--balance {
|
||||||
|
padding: 4px 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-pct-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-pct-btn {
|
||||||
|
padding: 6px 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-step {
|
||||||
|
width: 32px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-balance {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* paperDashboard 2열 그리드보다 우선 — 항상 세로 스택 */
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-price-qty-block {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-price-qty-block .top-field,
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-price-qty-block .top-pct-row--block {
|
||||||
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-reset,
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-submit {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 카드 높이 여유 있을 때: 간격 확대 + 하단 안내 텍스트 표시 */
|
||||||
|
@container trade-card (min-height: 440px) {
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-panel-fields {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-price-qty-block {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-meta {
|
||||||
|
display: flex;
|
||||||
|
font-size: 11px;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container trade-card (min-height: 520px) {
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-panel-fields {
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-price-qty-block {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* container query 미지원 fallback */
|
||||||
|
@supports not (container-type: size) {
|
||||||
|
@media (max-height: 820px) {
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-meta {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-panel-fields {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-price-qty-block {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-height: 821px) {
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-meta {
|
||||||
|
display: flex;
|
||||||
|
font-size: 11px;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-panel-fields {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--vtd .ptd-split-panel--right .ptd-order-card .top-price-qty-block {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ export class ChartManager {
|
|||||||
|
|
||||||
// ─── Theme ──────────────────────────────────────────────────────────────
|
// ─── Theme ──────────────────────────────────────────────────────────────
|
||||||
setTheme(theme: Theme): void {
|
setTheme(theme: Theme): void {
|
||||||
|
this.currentTheme = theme;
|
||||||
const t = getTheme(theme);
|
const t = getTheme(theme);
|
||||||
this.chart.applyOptions({
|
this.chart.applyOptions({
|
||||||
layout: { background: { type: ColorType.Solid, color: t.bgColor }, textColor: t.textColor },
|
layout: { background: { type: ColorType.Solid, color: t.bgColor }, textColor: t.textColor },
|
||||||
@@ -475,6 +476,59 @@ export class ChartManager {
|
|||||||
timeScale: { borderColor: t.borderColor },
|
timeScale: { borderColor: t.borderColor },
|
||||||
rightPriceScale: { borderColor: t.borderColor },
|
rightPriceScale: { borderColor: t.borderColor },
|
||||||
});
|
});
|
||||||
|
this._applyMainSeriesTheme(t);
|
||||||
|
this._applyVolumeSeriesTheme(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _applyMainSeriesTheme(t: ThemeTokens): void {
|
||||||
|
if (!this.mainSeries) return;
|
||||||
|
switch (this.currentChartType) {
|
||||||
|
case 'candlestick':
|
||||||
|
this.mainSeries.applyOptions({
|
||||||
|
upColor: t.upColor,
|
||||||
|
downColor: t.downColor,
|
||||||
|
borderUpColor: t.upColor,
|
||||||
|
borderDownColor: t.downColor,
|
||||||
|
wickUpColor: t.upColor,
|
||||||
|
wickDownColor: t.downColor,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'bar':
|
||||||
|
this.mainSeries.applyOptions({ upColor: t.upColor, downColor: t.downColor });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _applyVolumeSeriesTheme(t: ThemeTokens): void {
|
||||||
|
if (!this.volumeSeries || this.rawBars.length === 0) return;
|
||||||
|
this.volumeSeries.setData(this.rawBars.map(b => ({
|
||||||
|
time: b.time as Time,
|
||||||
|
value: b.volume,
|
||||||
|
color: b.close >= b.open ? `${t.upColor}88` : `${t.downColor}88`,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _discardIndicatorSeries(
|
||||||
|
seriesList: ISeriesApi<SeriesType>[],
|
||||||
|
options?: { cloudPlugin?: IchimokuCloudPlugin; fillPrimitive?: IndicatorFillPrimitive },
|
||||||
|
): void {
|
||||||
|
if (options?.cloudPlugin) {
|
||||||
|
for (const s of seriesList) {
|
||||||
|
try { s.detachPrimitive(options.cloudPlugin); } catch { /* ok */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options?.fillPrimitive && seriesList[0]) {
|
||||||
|
try { seriesList[0].detachPrimitive(options.fillPrimitive); } catch { /* ok */ }
|
||||||
|
}
|
||||||
|
for (const s of seriesList) {
|
||||||
|
try { this.chart.removeSeries(s); } catch { /* ok */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _indicatorLoadStale(loadGen: number): boolean {
|
||||||
|
return loadGen !== this._dataGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Indicators ─────────────────────────────────────────────────────────
|
// ─── Indicators ─────────────────────────────────────────────────────────
|
||||||
@@ -505,6 +559,10 @@ export class ChartManager {
|
|||||||
// ─── 일목균형표 특별 처리 ────────────────────────────────────────────
|
// ─── 일목균형표 특별 처리 ────────────────────────────────────────────
|
||||||
if (config.type === 'IchimokuCloud') {
|
if (config.type === 'IchimokuCloud') {
|
||||||
const { cloudPlugin, seriesMeta: ichiMeta } = await this._addIchimokuSeries(result, pane, config, seriesList);
|
const { cloudPlugin, seriesMeta: ichiMeta } = await this._addIchimokuSeries(result, pane, config, seriesList);
|
||||||
|
if (this._indicatorLoadStale(dataGenAtStart) || this.indicators.has(config.id)) {
|
||||||
|
this._discardIndicatorSeries(seriesList, { cloudPlugin });
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.indicators.set(config.id, { id: config.id, type: config.type, seriesList, seriesMeta: ichiMeta, paneIndex: pane, alertLines: [], hlineRefs: [], config, cloudPlugin });
|
this.indicators.set(config.id, { id: config.id, type: config.type, seriesList, seriesMeta: ichiMeta, paneIndex: pane, alertLines: [], hlineRefs: [], config, cloudPlugin });
|
||||||
this.applyIndicatorStyle(config);
|
this.applyIndicatorStyle(config);
|
||||||
return;
|
return;
|
||||||
@@ -593,6 +651,10 @@ export class ChartManager {
|
|||||||
id: config.id, type: config.type, seriesList, seriesMeta,
|
id: config.id, type: config.type, seriesList, seriesMeta,
|
||||||
paneIndex: pane, alertLines: [], hlineRefs, fillPrimitive, config,
|
paneIndex: pane, alertLines: [], hlineRefs, fillPrimitive, config,
|
||||||
};
|
};
|
||||||
|
if (this._indicatorLoadStale(dataGenAtStart) || this.indicators.has(config.id)) {
|
||||||
|
this._discardIndicatorSeries(seriesList, { fillPrimitive });
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.indicators.set(config.id, entry);
|
this.indicators.set(config.id, entry);
|
||||||
this.applyIndicatorStyle(config);
|
this.applyIndicatorStyle(config);
|
||||||
if (config.type === 'BollingerBands' && !indicatorHidden) {
|
if (config.type === 'BollingerBands' && !indicatorHidden) {
|
||||||
@@ -613,6 +675,10 @@ export class ChartManager {
|
|||||||
* reloadAll 과 달리 setData()를 호출하지 않으므로 메인 캔들 차트가 깜빡이지 않는다.
|
* reloadAll 과 달리 setData()를 호출하지 않으므로 메인 캔들 차트가 깜빡이지 않는다.
|
||||||
*/
|
*/
|
||||||
async reloadIndicatorsOnly(inds: import('../types').IndicatorConfig[]): Promise<void> {
|
async reloadIndicatorsOnly(inds: import('../types').IndicatorConfig[]): Promise<void> {
|
||||||
|
this.cancelPendingIndicatorUpdates();
|
||||||
|
this._dataGeneration += 1;
|
||||||
|
const loadGen = this._dataGeneration;
|
||||||
|
|
||||||
// ① 기존 보조지표 시리즈 모두 제거 (메인·볼륨 제외)
|
// ① 기존 보조지표 시리즈 모두 제거 (메인·볼륨 제외)
|
||||||
for (const entry of this.indicators.values()) {
|
for (const entry of this.indicators.values()) {
|
||||||
if (entry.cloudPlugin) {
|
if (entry.cloudPlugin) {
|
||||||
@@ -632,6 +698,7 @@ export class ChartManager {
|
|||||||
// ② 새 순서로 재추가 (병합 호스트 → 멤버 순)
|
// ② 새 순서로 재추가 (병합 호스트 → 멤버 순)
|
||||||
this._indRunning = false;
|
this._indRunning = false;
|
||||||
for (const ind of sortIndicatorsForPaneLoad(inds)) {
|
for (const ind of sortIndicatorsForPaneLoad(inds)) {
|
||||||
|
if (this._indicatorLoadStale(loadGen)) break;
|
||||||
await this.addIndicator(ind);
|
await this.addIndicator(ind);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,6 +1360,9 @@ export class ChartManager {
|
|||||||
* 심볼/타임프레임 변경 시 사용. 실시간 수신 시에는 appendBar()를 사용할 것.
|
* 심볼/타임프레임 변경 시 사용. 실시간 수신 시에는 appendBar()를 사용할 것.
|
||||||
*/
|
*/
|
||||||
async recalculateIndicators(newBars: OHLCVBar[]): Promise<void> {
|
async recalculateIndicators(newBars: OHLCVBar[]): Promise<void> {
|
||||||
|
this.cancelPendingIndicatorUpdates();
|
||||||
|
this._dataGeneration += 1;
|
||||||
|
const loadGen = this._dataGeneration;
|
||||||
this.rawBars = newBars;
|
this.rawBars = newBars;
|
||||||
|
|
||||||
const configs = Array.from(this.indicators.values()).map(e => e.config);
|
const configs = Array.from(this.indicators.values()).map(e => e.config);
|
||||||
@@ -1307,6 +1377,7 @@ export class ChartManager {
|
|||||||
this._removeExtraSubPanes();
|
this._removeExtraSubPanes();
|
||||||
|
|
||||||
for (const cfg of configs) {
|
for (const cfg of configs) {
|
||||||
|
if (this._indicatorLoadStale(loadGen)) break;
|
||||||
await this.addIndicator(cfg);
|
await this.addIndicator(cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user