보조지표 추가 팝업에서 지표 드래그 이동문제
This commit is contained in:
@@ -27,6 +27,11 @@ import {
|
|||||||
usePanelResize,
|
usePanelResize,
|
||||||
} from './strategyEditor/usePanelResize';
|
} from './strategyEditor/usePanelResize';
|
||||||
import '../styles/strategyEditorTheme.css';
|
import '../styles/strategyEditorTheme.css';
|
||||||
|
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';
|
||||||
@@ -188,11 +193,25 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
return null;
|
return null;
|
||||||
}, [tab, selectedBacktest, selectedLive, backtestDetail]);
|
}, [tab, selectedBacktest, selectedLive, backtestDetail]);
|
||||||
|
|
||||||
const centerTitle = tab === 'backtest' && selectedBacktest
|
const centerHead = useMemo(() => {
|
||||||
? `${selectedBacktest.strategyName || '전략'} · ${selectedBacktest.symbol.replace(/^KRW-/, '')} · ${selectedBacktest.timeframe}`
|
if (tab === 'backtest' && selectedBacktest) {
|
||||||
: tab === 'live' && selectedLive
|
const market = toUpbitMarket(selectedBacktest.symbol);
|
||||||
? `${selectedLive.strategyLabel} · ${selectedLive.symbol.replace(/^KRW-/, '')} · ${selectedLive.sourceLabel}`
|
return {
|
||||||
: '';
|
ko: getKoreanName(market),
|
||||||
|
strategy: selectedBacktest.strategyName || '전략 없음',
|
||||||
|
meta: selectedBacktest.timeframe,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (tab === 'live' && selectedLive) {
|
||||||
|
const market = toUpbitMarket(selectedLive.symbol);
|
||||||
|
return {
|
||||||
|
ko: getKoreanName(market),
|
||||||
|
strategy: selectedLive.strategyLabel,
|
||||||
|
meta: selectedLive.sourceLabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [tab, selectedBacktest, selectedLive]);
|
||||||
|
|
||||||
const footerTrades = equityData.trades;
|
const footerTrades = equityData.trades;
|
||||||
|
|
||||||
@@ -252,9 +271,13 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="btd-main">
|
<main className="btd-main">
|
||||||
{centerTitle && (
|
{centerHead && (
|
||||||
<div className="btd-main-head">
|
<div className="btd-main-head">
|
||||||
<h2 className="btd-main-title">{centerTitle}</h2>
|
<h2 className="btd-main-ko">{centerHead.ko}</h2>
|
||||||
|
<p className="btd-main-strategy">
|
||||||
|
{centerHead.strategy}
|
||||||
|
{centerHead.meta ? <span className="btd-main-meta"> · {centerHead.meta}</span> : null}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="btd-main-content">
|
<div className="btd-main-content">
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ const IndDropdown: React.FC<IndDropdownProps> = ({
|
|||||||
const [category, setCategory] = React.useState<IndicatorTabKey>('All');
|
const [category, setCategory] = React.useState<IndicatorTabKey>('All');
|
||||||
const [customTabs, setCustomTabs] = React.useState<IndicatorCustomTab[]>(() => loadCustomTabs());
|
const [customTabs, setCustomTabs] = React.useState<IndicatorCustomTab[]>(() => loadCustomTabs());
|
||||||
const [mainOrderTick, setMainOrderTick] = React.useState(0);
|
const [mainOrderTick, setMainOrderTick] = React.useState(0);
|
||||||
|
const [customTabsRevision, setCustomTabsRevision] = React.useState(0);
|
||||||
const [draggingType, setDraggingType] = React.useState<string | null>(null);
|
const [draggingType, setDraggingType] = React.useState<string | null>(null);
|
||||||
const [dragOverType, setDragOverType] = React.useState<string | null>(null);
|
const [dragOverType, setDragOverType] = React.useState<string | null>(null);
|
||||||
const [editorOpen, setEditorOpen] = React.useState(false);
|
const [editorOpen, setEditorOpen] = React.useState(false);
|
||||||
@@ -349,8 +350,15 @@ const IndDropdown: React.FC<IndDropdownProps> = ({
|
|||||||
|
|
||||||
const refreshCustomTabs = useCallback(() => {
|
const refreshCustomTabs = useCallback(() => {
|
||||||
setCustomTabs(loadCustomTabs());
|
setCustomTabs(loadCustomTabs());
|
||||||
|
setCustomTabsRevision(t => t + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onTabsChanged = () => refreshCustomTabs();
|
||||||
|
window.addEventListener('gc-indicator-custom-tabs-changed', onTabsChanged);
|
||||||
|
return () => window.removeEventListener('gc-indicator-custom-tabs-changed', onTabsChanged);
|
||||||
|
}, [refreshCustomTabs]);
|
||||||
|
|
||||||
const selectedCustomTab = React.useMemo(() => {
|
const selectedCustomTab = React.useMemo(() => {
|
||||||
const id = parseCustomTabKey(category);
|
const id = parseCustomTabKey(category);
|
||||||
return id ? customTabs.find(t => t.id === id) ?? null : null;
|
return id ? customTabs.find(t => t.id === id) ?? null : null;
|
||||||
@@ -390,13 +398,17 @@ const IndDropdown: React.FC<IndDropdownProps> = ({
|
|||||||
|
|
||||||
const tabTypeOrder = React.useMemo((): string[] | null => {
|
const tabTypeOrder = React.useMemo((): string[] | null => {
|
||||||
void mainOrderTick;
|
void mainOrderTick;
|
||||||
|
void customTabsRevision;
|
||||||
if (search.trim()) return null;
|
if (search.trim()) return null;
|
||||||
if (category === 'Main') return getOrderedMainIndicatorTypes();
|
if (category === 'Main') return getOrderedMainIndicatorTypes();
|
||||||
if (isCustomTabKey(category) && selectedCustomTab) {
|
if (isCustomTabKey(category)) {
|
||||||
return [...selectedCustomTab.indicatorTypes];
|
const id = parseCustomTabKey(category);
|
||||||
|
if (!id) return null;
|
||||||
|
const tab = customTabs.find(t => t.id === id);
|
||||||
|
return tab ? [...tab.indicatorTypes] : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [category, selectedCustomTab, customTabs, search, mainOrderTick]);
|
}, [category, customTabs, search, mainOrderTick, customTabsRevision]);
|
||||||
|
|
||||||
const canReorderList = tabTypeOrder != null && tabTypeOrder.length > 1;
|
const canReorderList = tabTypeOrder != null && tabTypeOrder.length > 1;
|
||||||
|
|
||||||
@@ -414,12 +426,19 @@ const IndDropdown: React.FC<IndDropdownProps> = ({
|
|||||||
setMainOrderTick(t => t + 1);
|
setMainOrderTick(t => t + 1);
|
||||||
saveIndicatorListOrder(mergeIndicatorListOrder(next, getSettingsIndicatorTypes()));
|
saveIndicatorListOrder(mergeIndicatorListOrder(next, getSettingsIndicatorTypes()));
|
||||||
onIndicatorOrderChange?.(next);
|
onIndicatorOrderChange?.(next);
|
||||||
} else if (isCustomTabKey(category) && selectedCustomTab) {
|
} else if (isCustomTabKey(category)) {
|
||||||
updateCustomTab(selectedCustomTab.id, { indicatorTypes: next });
|
const id = parseCustomTabKey(category);
|
||||||
refreshCustomTabs();
|
if (!id) return;
|
||||||
|
const updated = updateCustomTab(id, { indicatorTypes: next });
|
||||||
|
if (updated) {
|
||||||
|
setCustomTabs(prev => prev.map(t => (t.id === id ? updated : t)));
|
||||||
|
setCustomTabsRevision(t => t + 1);
|
||||||
|
} else {
|
||||||
|
refreshCustomTabs();
|
||||||
|
}
|
||||||
onIndicatorOrderChange?.(next);
|
onIndicatorOrderChange?.(next);
|
||||||
}
|
}
|
||||||
}, [tabTypeOrder, category, selectedCustomTab, refreshCustomTabs, onIndicatorOrderChange]);
|
}, [tabTypeOrder, category, refreshCustomTabs, onIndicatorOrderChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const clearDrag = () => {
|
const clearDrag = () => {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ 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, pct, pctAbs } from '../../utils/backtestUiUtils';
|
||||||
|
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';
|
||||||
@@ -82,12 +87,15 @@ export default function BacktestExecutionList({
|
|||||||
const ret = r.totalReturn ?? 0;
|
const ret = r.totalReturn ?? 0;
|
||||||
const positive = ret >= 0;
|
const positive = ret >= 0;
|
||||||
const active = selectedBacktestId === r.id;
|
const active = selectedBacktestId === r.id;
|
||||||
const sym = r.symbol.replace(/^KRW-/, '');
|
const market = toUpbitMarket(r.symbol);
|
||||||
|
const ko = getKoreanName(market);
|
||||||
|
const strategy = r.strategyName || '전략 없음';
|
||||||
return (
|
return (
|
||||||
<button key={r.id} type="button" className={`btd-history-card${active ? ' btd-history-card--active' : ''}`} onClick={() => onSelectBacktest(r)}>
|
<button key={r.id} type="button" className={`btd-history-card${active ? ' btd-history-card--active' : ''}`} onClick={() => onSelectBacktest(r)}>
|
||||||
<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-title">{r.strategyName || '전략 없음'} | {sym} | {r.timeframe}</span>
|
<span className="btd-history-ko">{ko}</span>
|
||||||
|
<span className="btd-history-strategy">{strategy} · {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">
|
||||||
@@ -109,12 +117,14 @@ export default function BacktestExecutionList({
|
|||||||
liveItems.map(item => {
|
liveItems.map(item => {
|
||||||
const positive = item.totalReturnPct >= 0;
|
const positive = item.totalReturnPct >= 0;
|
||||||
const active = selectedLiveId === item.id;
|
const active = selectedLiveId === item.id;
|
||||||
const sym = item.symbol.replace(/^KRW-/, '');
|
const market = toUpbitMarket(item.symbol);
|
||||||
|
const ko = getKoreanName(market);
|
||||||
return (
|
return (
|
||||||
<button key={item.id} type="button" className={`btd-history-card${active ? ' btd-history-card--active' : ''}`} onClick={() => onSelectLive(item)}>
|
<button key={item.id} type="button" className={`btd-history-card${active ? ' btd-history-card--active' : ''}`} onClick={() => onSelectLive(item)}>
|
||||||
<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-title">{item.strategyLabel} | {sym} | {item.sourceLabel}</span>
|
<span className="btd-history-ko">{ko}</span>
|
||||||
|
<span className="btd-history-strategy">{item.strategyLabel} · {item.sourceLabel}</span>
|
||||||
<span className="btd-history-date">{fmtShortTimestamp(item.createdAt)}</span>
|
<span className="btd-history-date">{fmtShortTimestamp(item.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="btd-history-card-side">
|
<div className="btd-history-card-side">
|
||||||
|
|||||||
@@ -970,13 +970,36 @@
|
|||||||
.btd-main-head {
|
.btd-main-head {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 8px 12px 0;
|
padding: 8px 12px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-main-title {
|
.btd-main-ko {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.88rem;
|
font-size: 0.92rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--btd-gold);
|
color: var(--se-text);
|
||||||
|
line-height: 1.25;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-main-strategy {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--se-text-muted);
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-main-meta {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--se-text-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-main-content {
|
.btd-main-content {
|
||||||
@@ -1121,13 +1144,27 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-history-title {
|
.btd-history-ko {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.72rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--se-text);
|
color: var(--se-text);
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-history-strategy {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--se-text-muted);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-history-date {
|
.btd-history-date {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function updateCustomTab(
|
|||||||
...patch,
|
...patch,
|
||||||
name: patch.name != null ? patch.name.trim() : tabs[idx].name,
|
name: patch.name != null ? patch.name.trim() : tabs[idx].name,
|
||||||
indicatorTypes: patch.indicatorTypes != null
|
indicatorTypes: patch.indicatorTypes != null
|
||||||
? [...new Set(patch.indicatorTypes)]
|
? [...patch.indicatorTypes]
|
||||||
: tabs[idx].indicatorTypes,
|
: tabs[idx].indicatorTypes,
|
||||||
};
|
};
|
||||||
tabs[idx] = next;
|
tabs[idx] = next;
|
||||||
|
|||||||
Reference in New Issue
Block a user