전략평가 화면 수정
This commit is contained in:
@@ -382,6 +382,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
appliedParams={appliedIndicatorParams}
|
appliedParams={appliedIndicatorParams}
|
||||||
onSave={handleSaveIndicatorParams}
|
onSave={handleSaveIndicatorParams}
|
||||||
onReset={handleResetIndicatorParams}
|
onReset={handleResetIndicatorParams}
|
||||||
|
applying={evalLoading || chartLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -268,11 +268,6 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
mgr.applyVisibleTimeRange(bar.time - barSec * 24, bar.time + barSec * 8);
|
mgr.applyVisibleTimeRange(bar.time - barSec * 24, bar.time + barSec * 8);
|
||||||
}, [chartMgr, bars]);
|
}, [chartMgr, bars]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (bars.length === 0) return;
|
|
||||||
scrollToBar(Math.min(Math.max(0, selectedBarIndex), bars.length - 1));
|
|
||||||
}, [selectedBarIndex, bars, scrollToBar]);
|
|
||||||
|
|
||||||
const onManagerReady = useCallback((mgr: ChartManager) => {
|
const onManagerReady = useCallback((mgr: ChartManager) => {
|
||||||
managerRef.current = mgr;
|
managerRef.current = mgr;
|
||||||
setChartMgr(mgr);
|
setChartMgr(mgr);
|
||||||
@@ -299,8 +294,10 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
|
|
||||||
const goLatest = useCallback(() => {
|
const goLatest = useCallback(() => {
|
||||||
if (bars.length === 0) return;
|
if (bars.length === 0) return;
|
||||||
onSelectedBarIndexChange(bars.length - 1);
|
const lastIdx = bars.length - 1;
|
||||||
}, [bars.length, onSelectedBarIndexChange]);
|
onSelectedBarIndexChange(lastIdx);
|
||||||
|
scrollToBar(lastIdx);
|
||||||
|
}, [bars.length, onSelectedBarIndexChange, scrollToBar]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mgr = chartMgr;
|
const mgr = chartMgr;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface Props {
|
|||||||
appliedParams: EvalIndicatorParams | null;
|
appliedParams: EvalIndicatorParams | null;
|
||||||
onSave: (params: EvalIndicatorParams) => void;
|
onSave: (params: EvalIndicatorParams) => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
applying?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditableParamKeys(
|
function getEditableParamKeys(
|
||||||
@@ -47,6 +48,7 @@ const StrategyEvaluationSettingsTab: React.FC<Props> = ({
|
|||||||
appliedParams,
|
appliedParams,
|
||||||
onSave,
|
onSave,
|
||||||
onReset,
|
onReset,
|
||||||
|
applying = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [draft, setDraft] = useState<EvalIndicatorParams>({});
|
const [draft, setDraft] = useState<EvalIndicatorParams>({});
|
||||||
const [expanded, setExpanded] = useState<string | null>(null);
|
const [expanded, setExpanded] = useState<string | null>(null);
|
||||||
@@ -56,23 +58,31 @@ const StrategyEvaluationSettingsTab: React.FC<Props> = ({
|
|||||||
[strategy],
|
[strategy],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resolveBaseline = useCallback((): EvalIndicatorParams => {
|
||||||
|
if (!strategy) return {};
|
||||||
|
return appliedParams ?? buildEvalParamsFromStrategy(strategy, getParams);
|
||||||
|
}, [strategy, appliedParams, getParams]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!strategy) {
|
if (!strategy) {
|
||||||
setDraft({});
|
setDraft({});
|
||||||
setExpanded(null);
|
setExpanded(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const baseline = appliedParams ?? buildEvalParamsFromStrategy(strategy, getParams);
|
setDraft(JSON.parse(JSON.stringify(resolveBaseline())) as EvalIndicatorParams);
|
||||||
setDraft(JSON.parse(JSON.stringify(baseline)) as EvalIndicatorParams);
|
|
||||||
setExpanded(indicatorTypes[0] ?? null);
|
setExpanded(indicatorTypes[0] ?? null);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [strategy?.id, appliedParams, indicatorTypes.join(',')]);
|
}, [strategy?.id, appliedParams, indicatorTypes.join(',')]);
|
||||||
|
|
||||||
const dirty = useMemo(
|
const dirty = useMemo(
|
||||||
() => hasEvalParamsDiff(draft, appliedParams),
|
() => hasEvalParamsDiff(draft, resolveBaseline()),
|
||||||
[draft, appliedParams],
|
[draft, resolveBaseline],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleApply = useCallback(() => {
|
||||||
|
onSave(JSON.parse(JSON.stringify(draft)) as EvalIndicatorParams);
|
||||||
|
}, [draft, onSave]);
|
||||||
|
|
||||||
const patchParam = useCallback((type: string, key: string, raw: string | boolean) => {
|
const patchParam = useCallback((type: string, key: string, raw: string | boolean) => {
|
||||||
setDraft(prev => {
|
setDraft(prev => {
|
||||||
const current = prev[type] ?? {};
|
const current = prev[type] ?? {};
|
||||||
@@ -115,8 +125,19 @@ const StrategyEvaluationSettingsTab: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="seval-settings-tab">
|
<div className="seval-settings-tab">
|
||||||
<div className="seval-settings-head">
|
<div className="seval-settings-head">
|
||||||
<p className="seval-settings-strategy">{strategyName}</p>
|
<div className="seval-settings-head-row">
|
||||||
<p className="seval-settings-hint">지표 값을 변경 후 저장하면 차트·일치율이 재계산됩니다</p>
|
<p className="seval-settings-strategy">{strategyName}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="seval-settings-apply"
|
||||||
|
disabled={!dirty || applying}
|
||||||
|
onClick={handleApply}
|
||||||
|
title="변경된 지표 값을 차트·일치율에 반영"
|
||||||
|
>
|
||||||
|
{applying ? '적용 중…' : '적용'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="seval-settings-hint">지표 값을 변경한 뒤 적용을 누르면 차트·일치율이 갱신됩니다</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="seval-settings-list">
|
<div className="seval-settings-list">
|
||||||
@@ -163,18 +184,10 @@ const StrategyEvaluationSettingsTab: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="seval-settings-actions">
|
<div className="seval-settings-actions">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="seval-settings-save"
|
|
||||||
disabled={!dirty}
|
|
||||||
onClick={() => onSave(JSON.parse(JSON.stringify(draft)) as EvalIndicatorParams)}
|
|
||||||
>
|
|
||||||
저장 · 재계산
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="seval-settings-reset"
|
className="seval-settings-reset"
|
||||||
disabled={!appliedParams && !dirty}
|
disabled={(!appliedParams && !dirty) || applying}
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
>
|
>
|
||||||
초기화
|
초기화
|
||||||
|
|||||||
@@ -1004,11 +1004,43 @@
|
|||||||
padding: 4px 2px 8px;
|
padding: 4px 2px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seval-settings-head-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.seval-settings-strategy {
|
.seval-settings-strategy {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--se-text);
|
color: var(--se-text);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-settings-apply {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--btd-gold, var(--se-gold)) 45%, var(--se-border));
|
||||||
|
background: color-mix(in srgb, var(--btd-gold, var(--se-gold)) 16%, var(--se-input-bg));
|
||||||
|
color: var(--btd-gold, var(--se-gold));
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-settings-apply:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--btd-gold, var(--se-gold)) 24%, var(--se-input-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.seval-settings-apply:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seval-settings-hint {
|
.seval-settings-hint {
|
||||||
@@ -1089,7 +1121,6 @@
|
|||||||
border-top: 1px solid var(--btd-divider, var(--se-border));
|
border-top: 1px solid var(--btd-divider, var(--se-border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.seval-settings-save,
|
|
||||||
.seval-settings-reset {
|
.seval-settings-reset {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 9px 10px;
|
padding: 9px 10px;
|
||||||
@@ -1097,26 +1128,16 @@
|
|||||||
font-size: 0.74rem;
|
font-size: 0.74rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.seval-settings-save {
|
|
||||||
border: 1px solid color-mix(in srgb, var(--btd-gold, var(--se-gold)) 45%, var(--se-border));
|
|
||||||
background: color-mix(in srgb, var(--btd-gold, var(--se-gold)) 12%, var(--se-input-bg));
|
|
||||||
color: var(--btd-gold, var(--se-gold));
|
|
||||||
}
|
|
||||||
|
|
||||||
.seval-settings-save:disabled,
|
|
||||||
.seval-settings-reset:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seval-settings-reset {
|
|
||||||
border: 1px solid var(--btd-divider, var(--se-border));
|
border: 1px solid var(--btd-divider, var(--se-border));
|
||||||
background: var(--btd-surface, var(--se-input-bg));
|
background: var(--btd-surface, var(--se-input-bg));
|
||||||
color: var(--se-text-muted);
|
color: var(--se-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seval-settings-reset:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
@media (max-width: 960px) {
|
||||||
.seval-chart-hint {
|
.seval-chart-hint {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user