전략평가 오류 수정
This commit is contained in:
@@ -49,10 +49,15 @@ public class BacktestingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long userId = TradingControllerSupport.requireRegisteredUser(headers);
|
long userId = TradingControllerSupport.requireRegisteredUser(headers);
|
||||||
|
String deviceId = headers.get("x-device-id");
|
||||||
|
if (deviceId == null || deviceId.isBlank()) {
|
||||||
|
deviceId = req.getDeviceId();
|
||||||
|
}
|
||||||
|
req.setUserId(userId);
|
||||||
|
req.setDeviceId(deviceId);
|
||||||
|
|
||||||
java.util.Map<String, java.util.Map<String, Object>> dbParams =
|
java.util.Map<String, java.util.Map<String, Object>> dbParams =
|
||||||
indicatorSettingsService.getAll(userId, null);
|
indicatorSettingsService.getAll(userId, deviceId);
|
||||||
if (!dbParams.isEmpty()) {
|
|
||||||
java.util.Map<String, java.util.Map<String, Object>> merged =
|
java.util.Map<String, java.util.Map<String, Object>> merged =
|
||||||
new java.util.HashMap<>(dbParams);
|
new java.util.HashMap<>(dbParams);
|
||||||
if (req.getIndicatorParams() != null) {
|
if (req.getIndicatorParams() != null) {
|
||||||
@@ -64,6 +69,7 @@ public class BacktestingController {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!merged.isEmpty()) {
|
||||||
req.setIndicatorParams(merged);
|
req.setIndicatorParams(merged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,4 +63,7 @@ public class BacktestRequest {
|
|||||||
|
|
||||||
/** 디바이스 ID (결과 저장용) */
|
/** 디바이스 ID (결과 저장용) */
|
||||||
private String deviceId;
|
private String deviceId;
|
||||||
|
|
||||||
|
/** 인증 사용자 ID — 컨트롤러에서 설정, 지표 설정 조회용 */
|
||||||
|
private Long userId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,13 +77,16 @@ public class BacktestingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DB 저장 인디케이터 설정을 기본으로 로드 (디바이스별 저장값 → 하드코딩 기본값 방지)
|
// DB 저장 인디케이터 설정 — live-conditions/evaluate 와 동일 (userId + deviceId)
|
||||||
Map<String, Map<String, Object>> dbParams = Map.of();
|
Map<String, Map<String, Object>> dbParams = Map.of();
|
||||||
|
Map<String, Map<String, Object>> visual = Map.of();
|
||||||
if (req.getDeviceId() != null && !req.getDeviceId().isBlank()) {
|
if (req.getDeviceId() != null && !req.getDeviceId().isBlank()) {
|
||||||
try {
|
try {
|
||||||
dbParams = indicatorSettingsService.getAll(null, req.getDeviceId());
|
dbParams = indicatorSettingsService.getAll(req.getUserId(), req.getDeviceId());
|
||||||
|
visual = indicatorSettingsService.getAllVisual(req.getUserId(), req.getDeviceId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[Backtest] 인디케이터 설정 DB 로드 실패 (deviceId={}): {}", req.getDeviceId(), e.getMessage());
|
log.warn("[Backtest] 인디케이터 설정 DB 로드 실패 (userId={}, deviceId={}): {}",
|
||||||
|
req.getUserId(), req.getDeviceId(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 요청에 포함된 파라미터가 있으면 DB 값 위에 덮어씀 (요청값 우선)
|
// 요청에 포함된 파라미터가 있으면 DB 값 위에 덮어씀 (요청값 우선)
|
||||||
@@ -111,9 +114,12 @@ public class BacktestingService {
|
|||||||
Map<String, BarSeries> seriesOverrides = buildSeriesOverrides(
|
Map<String, BarSeries> seriesOverrides = buildSeriesOverrides(
|
||||||
series, primaryTf, buyDsl, sellDsl);
|
series, primaryTf, buyDsl, sellDsl);
|
||||||
|
|
||||||
Rule entryRule = adapter.toRule(buyDsl, series, params, seriesOverrides);
|
StrategyDslToTa4jAdapter.RuleBuildContext ruleCtx =
|
||||||
|
new StrategyDslToTa4jAdapter.RuleBuildContext(
|
||||||
|
series, params, visual, null, null, false, seriesOverrides);
|
||||||
|
Rule entryRule = adapter.toRule(buyDsl, ruleCtx);
|
||||||
Rule baseExitRule = (sellDsl != null && !sellDsl.isNull())
|
Rule baseExitRule = (sellDsl != null && !sellDsl.isNull())
|
||||||
? adapter.toRule(sellDsl, series, params, seriesOverrides)
|
? adapter.toRule(sellDsl, ruleCtx)
|
||||||
: new BooleanRule(false);
|
: new BooleanRule(false);
|
||||||
Rule exitRule = buildExitRule(baseExitRule, series, cfg);
|
Rule exitRule = buildExitRule(baseExitRule, series, cfg);
|
||||||
|
|
||||||
@@ -199,23 +205,21 @@ public class BacktestingService {
|
|||||||
double exitPrice = getPrice(series, req.getBars(), i, cfg.getExitPriceType());
|
double exitPrice = getPrice(series, req.getBars(), i, cfg.getExitPriceType());
|
||||||
long time = barStartEpoch(series, i);
|
long time = barStartEpoch(series, i);
|
||||||
|
|
||||||
// ── SIGNAL_ONLY: 조건 충족 '시작' 봉만 시그널 (GTE 유지형 연속 중복 방지) ──
|
// ── SIGNAL_ONLY: 조건 충족 봉마다 시그널 (live-conditions 충족과 동일 기준) ──
|
||||||
if (signalOnly) {
|
if (signalOnly) {
|
||||||
boolean entrySatisfied = entryRule.isSatisfied(i);
|
boolean entrySatisfied = entryRule.isSatisfied(i);
|
||||||
boolean exitSatisfied = exitRule.isSatisfied(i);
|
boolean exitSatisfied = exitRule.isSatisfied(i);
|
||||||
boolean entryEdge = entrySatisfied && (i <= loopStart || !entryRule.isSatisfied(i - 1));
|
if (entrySatisfied) {
|
||||||
boolean exitEdge = exitSatisfied && (i <= loopStart || !exitRule.isSatisfied(i - 1));
|
|
||||||
if (entryEdge) {
|
|
||||||
double effEntry = applySlippage(closePrice, cfg, true);
|
double effEntry = applySlippage(closePrice, cfg, true);
|
||||||
String buyType = "SHORT".equals(direction) ? "SHORT_ENTRY" : "BUY";
|
String buyType = "SHORT".equals(direction) ? "SHORT_ENTRY" : "BUY";
|
||||||
signals.add(buildFillSignal(time, buyType, effEntry, i, 1.0));
|
signals.add(buildFillSignal(time, buyType, effEntry, i, 1.0));
|
||||||
log.debug("[Backtest:SIGNAL_ONLY] BUY(edge) @ bar={} price={}", i, effEntry);
|
log.debug("[Backtest:SIGNAL_ONLY] BUY @ bar={} price={}", i, effEntry);
|
||||||
}
|
}
|
||||||
if (exitEdge) {
|
if (exitSatisfied) {
|
||||||
double effExit = applySlippage(exitPrice, cfg, false);
|
double effExit = applySlippage(exitPrice, cfg, false);
|
||||||
String sellType = "SHORT".equals(direction) ? "SHORT_EXIT" : "SELL";
|
String sellType = "SHORT".equals(direction) ? "SHORT_EXIT" : "SELL";
|
||||||
signals.add(buildFillSignal(time, sellType, effExit, i, 1.0));
|
signals.add(buildFillSignal(time, sellType, effExit, i, 1.0));
|
||||||
log.debug("[Backtest:SIGNAL_ONLY] SELL(edge) @ bar={} price={}", i, effExit);
|
log.debug("[Backtest:SIGNAL_ONLY] SELL @ bar={} price={}", i, effExit);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ public class StrategySignalDeterminer {
|
|||||||
int index, String positionMode) {
|
int index, String positionMode) {
|
||||||
try {
|
try {
|
||||||
if ("SIGNAL_ONLY".equals(positionMode)) {
|
if ("SIGNAL_ONLY".equals(positionMode)) {
|
||||||
boolean enterOk = entryRule != null && isRisingEdge(entryRule, index);
|
boolean enterOk = entryRule != null && entryRule.isSatisfied(index);
|
||||||
boolean exitOk = exitRule != null && isRisingEdge(exitRule, index);
|
boolean exitOk = exitRule != null && exitRule.isSatisfied(index);
|
||||||
if (enterOk) return "BUY";
|
if (enterOk) return "BUY";
|
||||||
if (exitOk) return "SELL";
|
if (exitOk) return "SELL";
|
||||||
return "NONE";
|
return "NONE";
|
||||||
@@ -84,15 +84,10 @@ public class StrategySignalDeterminer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String determineSignalOnly(Strategy strategy, int index) {
|
private String determineSignalOnly(Strategy strategy, int index) {
|
||||||
if (isRisingEdge(strategy.getEntryRule(), index)) return "BUY";
|
Rule entry = strategy.getEntryRule();
|
||||||
if (isRisingEdge(strategy.getExitRule(), index)) return "SELL";
|
Rule exit = strategy.getExitRule();
|
||||||
|
if (entry != null && entry.isSatisfied(index)) return "BUY";
|
||||||
|
if (exit != null && exit.isSatisfied(index)) return "SELL";
|
||||||
return "NONE";
|
return "NONE";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GTE 등 유지형 조건 — 충족 시작 봉만 true (연속 시그널 방지) */
|
|
||||||
private boolean isRisingEdge(Rule rule, int index) {
|
|
||||||
if (rule == null || !rule.isSatisfied(index)) return false;
|
|
||||||
if (index <= 0) return true;
|
|
||||||
return !rule.isSatisfied(index - 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1919,12 +1919,69 @@ html.desktop-client .tmb-logo-version {
|
|||||||
}
|
}
|
||||||
.ind-tab-editor-footer {
|
.ind-tab-editor-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.ind-tab-editor-error {
|
||||||
|
margin-right: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 인앱 확인 모달 (데스크톱 웹뷰에서 window.confirm 미동작 대체) */
|
||||||
|
.gc-confirm-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2200;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
.gc-confirm-box {
|
||||||
|
width: min(360px, calc(100vw - 24px));
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
|
||||||
|
padding: 18px 18px 14px;
|
||||||
|
}
|
||||||
|
.gc-confirm-msg {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.gc-confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.gc-confirm-btn {
|
||||||
|
padding: 7px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.gc-confirm-btn.cancel {
|
||||||
|
background: var(--bg3);
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
.gc-confirm-btn.cancel:hover { color: var(--text); }
|
||||||
|
.gc-confirm-btn.ok {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.gc-confirm-btn.ok:hover { filter: brightness(1.08); }
|
||||||
.ind-tab-editor-btn {
|
.ind-tab-editor-btn {
|
||||||
padding: 7px 16px;
|
padding: 7px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
|
|||||||
const [name, setName] = useState(initialName);
|
const [name, setName] = useState(initialName);
|
||||||
const [selected, setSelected] = useState<Set<string>>(() => new Set(initialTypes));
|
const [selected, setSelected] = useState<Set<string>>(() => new Set(initialTypes));
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
@@ -64,13 +65,14 @@ const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
|
|||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const trimmed = name.trim();
|
const trimmed = name.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
window.alert('탭 이름을 입력해 주세요.');
|
setError('탭 이름을 입력해 주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selected.size === 0) {
|
if (selected.size === 0) {
|
||||||
window.alert('탭에 표시할 지표를 하나 이상 선택해 주세요.');
|
setError('탭에 표시할 지표를 하나 이상 선택해 주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setError('');
|
||||||
const ordered = INDICATOR_REGISTRY
|
const ordered = INDICATOR_REGISTRY
|
||||||
.filter(d => selected.has(d.type))
|
.filter(d => selected.has(d.type))
|
||||||
.map(d => d.type);
|
.map(d => d.type);
|
||||||
@@ -96,7 +98,7 @@ const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
|
|||||||
<input
|
<input
|
||||||
className="ind-tab-editor-input"
|
className="ind-tab-editor-input"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e => { setName(e.target.value); if (error) setError(''); }}
|
||||||
placeholder="예: 내 지표"
|
placeholder="예: 내 지표"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
@@ -156,6 +158,7 @@ const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ind-tab-editor-footer">
|
<div className="ind-tab-editor-footer">
|
||||||
|
{error && <span className="ind-tab-editor-error">{error}</span>}
|
||||||
<button type="button" className="ind-tab-editor-btn cancel" onClick={onClose}>
|
<button type="button" className="ind-tab-editor-btn cancel" onClick={onClose}>
|
||||||
취소
|
취소
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* 실시간 차트 툴바와 동일한 보조지표 추가·제거 패널 (ind-panel)
|
* 실시간 차트 툴바와 동일한 보조지표 추가·제거 패널 (ind-panel)
|
||||||
*/
|
*/
|
||||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||||
import { INDICATOR_REGISTRY, type IndicatorDef } from '../utils/indicatorRegistry';
|
import { INDICATOR_REGISTRY, type IndicatorDef } from '../utils/indicatorRegistry';
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +13,12 @@ import {
|
|||||||
parseCustomTabKey,
|
parseCustomTabKey,
|
||||||
type IndicatorTabKey,
|
type IndicatorTabKey,
|
||||||
} from '../utils/indicatorMainTab';
|
} from '../utils/indicatorMainTab';
|
||||||
import { confirmAndApplyIndicatorTab } from '../utils/indicatorTabApply';
|
import {
|
||||||
|
applyIndicatorTab,
|
||||||
|
resolveIndicatorTabApplyTypes,
|
||||||
|
resolveIndicatorTabApplyLabel,
|
||||||
|
type IndicatorTabApplySource,
|
||||||
|
} from '../utils/indicatorTabApply';
|
||||||
import {
|
import {
|
||||||
getOrderedMainIndicatorTypes,
|
getOrderedMainIndicatorTypes,
|
||||||
saveMainTabOrder,
|
saveMainTabOrder,
|
||||||
@@ -85,6 +91,7 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
|
|||||||
const [dragOverType, setDragOverType] = useState<string | null>(null);
|
const [dragOverType, setDragOverType] = useState<string | null>(null);
|
||||||
const [editorOpen, setEditorOpen] = useState(false);
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
const [editorMode, setEditorMode] = useState<'create' | 'edit'>('create');
|
const [editorMode, setEditorMode] = useState<'create' | 'edit'>('create');
|
||||||
|
const [confirmState, setConfirmState] = useState<{ message: string; onConfirm: () => void } | null>(null);
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
const searchFocusedRef = useRef(false);
|
const searchFocusedRef = useRef(false);
|
||||||
|
|
||||||
@@ -117,10 +124,15 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
|
|||||||
|
|
||||||
const handleDeleteTab = () => {
|
const handleDeleteTab = () => {
|
||||||
if (!selectedCustomTab) return;
|
if (!selectedCustomTab) return;
|
||||||
if (!window.confirm(`"${selectedCustomTab.name}" 탭을 삭제할까요?`)) return;
|
const tab = selectedCustomTab;
|
||||||
deleteCustomTab(selectedCustomTab.id);
|
setConfirmState({
|
||||||
|
message: `"${tab.name}" 탭을 삭제할까요?`,
|
||||||
|
onConfirm: () => {
|
||||||
|
deleteCustomTab(tab.id);
|
||||||
refreshCustomTabs();
|
refreshCustomTabs();
|
||||||
setCategory('All');
|
setCategory('All');
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditorSave = (name: string, indicatorTypes: string[]) => {
|
const handleEditorSave = (name: string, indicatorTypes: string[]) => {
|
||||||
@@ -205,14 +217,24 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
|
|||||||
|
|
||||||
const addAllDisabled = category === 'All' || !addAllContext || addAllContext.types.length === 0;
|
const addAllDisabled = category === 'All' || !addAllContext || addAllContext.types.length === 0;
|
||||||
|
|
||||||
|
const requestApplyTab = (source: IndicatorTabApplySource) => {
|
||||||
|
const validTypes = resolveIndicatorTabApplyTypes(source);
|
||||||
|
if (validTypes.length === 0) return;
|
||||||
|
const label = resolveIndicatorTabApplyLabel(source);
|
||||||
|
setConfirmState({
|
||||||
|
message: `기존 보조지표를 모두 제거하고 ${label} 탭의 지표 ${validTypes.length}개로 교체하시겠습니까?`,
|
||||||
|
onConfirm: () => applyIndicatorTab(source, onAddMany, onAdd),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddAllInTab = () => {
|
const handleAddAllInTab = () => {
|
||||||
if (!addAllContext || addAllContext.types.length === 0) return;
|
if (!addAllContext || addAllContext.types.length === 0) return;
|
||||||
if (category === 'Main') {
|
if (category === 'Main') {
|
||||||
confirmAndApplyIndicatorTab({ kind: 'main' }, onAddMany, onAdd);
|
requestApplyTab({ kind: 'main' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isCustomTabKey(category) && selectedCustomTab) {
|
if (isCustomTabKey(category) && selectedCustomTab) {
|
||||||
confirmAndApplyIndicatorTab({ kind: 'custom', tab: selectedCustomTab }, onAddMany, onAdd);
|
requestApplyTab({ kind: 'custom', tab: selectedCustomTab });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,6 +251,10 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key !== 'Escape') return;
|
if (e.key !== 'Escape') return;
|
||||||
|
if (confirmState) {
|
||||||
|
setConfirmState(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (editorOpen) {
|
if (editorOpen) {
|
||||||
setEditorOpen(false);
|
setEditorOpen(false);
|
||||||
return;
|
return;
|
||||||
@@ -237,7 +263,7 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
|
|||||||
};
|
};
|
||||||
window.addEventListener('keydown', onKey);
|
window.addEventListener('keydown', onKey);
|
||||||
return () => window.removeEventListener('keydown', onKey);
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
}, [onClose, editorOpen]);
|
}, [onClose, editorOpen, confirmState]);
|
||||||
|
|
||||||
const isActive = (type: string) => activeIndicators.some(a => a.type === type);
|
const isActive = (type: string) => activeIndicators.some(a => a.type === type);
|
||||||
|
|
||||||
@@ -535,6 +561,38 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{confirmState && ReactDOM.createPortal(
|
||||||
|
<div
|
||||||
|
className="gc-confirm-overlay"
|
||||||
|
onMouseDown={e => { if (e.target === e.currentTarget) setConfirmState(null); }}
|
||||||
|
>
|
||||||
|
<div className="gc-confirm-box" onMouseDown={e => e.stopPropagation()}>
|
||||||
|
<p className="gc-confirm-msg">{confirmState.message}</p>
|
||||||
|
<div className="gc-confirm-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gc-confirm-btn cancel"
|
||||||
|
onClick={() => setConfirmState(null)}
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gc-confirm-btn ok"
|
||||||
|
onClick={() => {
|
||||||
|
const fn = confirmState.onConfirm;
|
||||||
|
setConfirmState(null);
|
||||||
|
fn();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
확인
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -139,14 +139,10 @@ const StrategyEvaluationChart: React.FC<Props> = ({
|
|||||||
return { candle: flex.candle, aux: flex.aux };
|
return { candle: flex.candle, aux: flex.aux };
|
||||||
}, [auxPaneCount]);
|
}, [auxPaneCount]);
|
||||||
|
|
||||||
/** 저장된 오버라이드가 있을 때만 백테스트에 전달 — 미설정 시 live-conditions 와 같이 서버 DB 사용 */
|
/** 전략 DSL 지표 파라미터 — live-conditions/evaluate·차트와 동일 소스 */
|
||||||
const indicatorParams = useMemo(
|
const indicatorParams = useMemo(
|
||||||
() => (
|
() => (strategy ? buildEvalParamsFromStrategy(strategy, getParams) : undefined),
|
||||||
strategy && appliedIndicatorParams
|
[strategy, getParams],
|
||||||
? buildEvalParamsFromStrategy(strategy, getParams)
|
|
||||||
: undefined
|
|
||||||
),
|
|
||||||
[strategy, getParams, appliedIndicatorParams],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolveInitialDisplayCount = useCallback(
|
const resolveInitialDisplayCount = useCallback(
|
||||||
|
|||||||
Reference in New Issue
Block a user