전략 시간봉 오류 수정
This commit is contained in:
+39
-7
@@ -68,8 +68,20 @@ public class StrategyConditionTimeframeService {
|
|||||||
ensureDslRepaired(strategy);
|
ensureDslRepaired(strategy);
|
||||||
|
|
||||||
Set<String> out = new LinkedHashSet<>();
|
Set<String> out = new LinkedHashSet<>();
|
||||||
collectFromJson(strategy.getBuyConditionJson(), out, strategy.getName());
|
boolean buyScoped = collectStartScopeFromJson(strategy.getBuyConditionJson(), out);
|
||||||
collectFromJson(strategy.getSellConditionJson(), out, strategy.getName());
|
boolean sellScoped = collectStartScopeFromJson(strategy.getSellConditionJson(), out);
|
||||||
|
|
||||||
|
if (!out.isEmpty()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레거시: START(TIMEFRAME) 래핑 없는 DSL
|
||||||
|
if (!buyScoped) {
|
||||||
|
collectLegacyFromJson(strategy.getBuyConditionJson(), out, strategy.getName());
|
||||||
|
}
|
||||||
|
if (!sellScoped) {
|
||||||
|
collectLegacyFromJson(strategy.getSellConditionJson(), out, strategy.getName());
|
||||||
|
}
|
||||||
|
|
||||||
if (out.isEmpty()) {
|
if (out.isEmpty()) {
|
||||||
String fromName = StrategyDslTimeframeNormalizer.inferFromStrategyName(strategy.getName());
|
String fromName = StrategyDslTimeframeNormalizer.inferFromStrategyName(strategy.getName());
|
||||||
@@ -106,19 +118,39 @@ public class StrategyConditionTimeframeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectFromJson(String json, Set<String> out, String strategyName) {
|
/**
|
||||||
if (json == null || json.isBlank()) return;
|
* START 루트 TIMEFRAME(candleTypes)만 수집 — 조건 노드 leftCandleType·내부 1m 기본값은 제외.
|
||||||
|
*
|
||||||
|
* @return JSON 에 START 스코프가 있으면 true (빈 조건이어도)
|
||||||
|
*/
|
||||||
|
private boolean collectStartScopeFromJson(String json, Set<String> out) {
|
||||||
|
if (json == null || json.isBlank()) return false;
|
||||||
try {
|
try {
|
||||||
collectFromNode(objectMapper.readTree(json), out, strategyName);
|
return collectStartScopeFromNode(objectMapper.readTree(json), out);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[StrategyTimeframes] JSON 파싱 실패: {}", e.getMessage());
|
log.warn("[StrategyTimeframes] JSON 파싱 실패: {}", e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean collectStartScopeFromNode(JsonNode node, Set<String> out) {
|
||||||
|
if (node == null || node.isNull()) return false;
|
||||||
|
return collectTimeframesFromNode(node, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 레거시 DSL — START 래핑 없을 때만 조건 트리·전략명에서 추론 */
|
||||||
|
private void collectLegacyFromJson(String json, Set<String> out, String strategyName) {
|
||||||
|
if (json == null || json.isBlank()) return;
|
||||||
|
try {
|
||||||
|
collectLegacyFromNode(objectMapper.readTree(json), out, strategyName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[StrategyTimeframes] legacy JSON 파싱 실패: {}", e.getMessage());
|
||||||
addFallbackTimeframe(out, strategyName);
|
addFallbackTimeframe(out, strategyName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectFromNode(JsonNode node, Set<String> out, String strategyName) {
|
private void collectLegacyFromNode(JsonNode node, Set<String> out, String strategyName) {
|
||||||
if (node == null || node.isNull()) return;
|
if (node == null || node.isNull()) return;
|
||||||
if (collectTimeframesFromNode(node, out)) return;
|
|
||||||
|
|
||||||
Set<String> meaningful = StrategyDslTimeframeNormalizer.collectMeaningfulCandleTypes(node);
|
Set<String> meaningful = StrategyDslTimeframeNormalizer.collectMeaningfulCandleTypes(node);
|
||||||
if (!meaningful.isEmpty()) {
|
if (!meaningful.isEmpty()) {
|
||||||
|
|||||||
+32
@@ -101,6 +101,38 @@ class StrategyConditionTimeframeServiceTest {
|
|||||||
assertTrue(service.usesTimeframe(3L, "3m"));
|
assertTrue(service.usesTimeframe(3L, "3m"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void collectForStrategy_buyStartScopeIgnoresSellLegacy1m() {
|
||||||
|
GcStrategy mixed = new GcStrategy();
|
||||||
|
mixed.setId(5L);
|
||||||
|
mixed.setBuyConditionJson("""
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleType": "3m",
|
||||||
|
"candleTypes": ["3m", "5m"],
|
||||||
|
"children": [{
|
||||||
|
"type": "CONDITION",
|
||||||
|
"condition": { "indicatorType": "STOCHASTIC", "period": 14 }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
mixed.setSellConditionJson("""
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleType": "1m",
|
||||||
|
"children": [{
|
||||||
|
"type": "CONDITION",
|
||||||
|
"condition": { "indicatorType": "STOCHASTIC", "period": 14 }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
when(strategyRepo.findById(5L)).thenReturn(Optional.of(mixed));
|
||||||
|
|
||||||
|
Set<String> tfs = service.collectForStrategy(5L);
|
||||||
|
assertEquals(Set.of("3m", "5m"), tfs);
|
||||||
|
assertFalse(service.usesTimeframe(5L, "1m"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void collectForStrategy_legacyTreeDefaultsTo1m() {
|
void collectForStrategy_legacyTreeDefaultsTo1m() {
|
||||||
GcStrategy legacy = new GcStrategy();
|
GcStrategy legacy = new GcStrategy();
|
||||||
|
|||||||
+10
-3
@@ -1361,7 +1361,7 @@ function App() {
|
|||||||
cfg = { ...fromConfig, id: newIndId(), type: def.type };
|
cfg = { ...fromConfig, id: newIndId(), type: def.type };
|
||||||
} else {
|
} else {
|
||||||
const params = getParams(type, def.defaultParams);
|
const params = getParams(type, def.defaultParams);
|
||||||
const { plots, hlines, cloudColors } = getVisualConfig(type, def.plots, def.hlines);
|
const { plots, hlines, cloudColors, bandBackground } = getVisualConfig(type, def.plots, def.hlines);
|
||||||
cfg = {
|
cfg = {
|
||||||
id: newIndId(),
|
id: newIndId(),
|
||||||
type: def.type,
|
type: def.type,
|
||||||
@@ -1369,6 +1369,7 @@ function App() {
|
|||||||
plots,
|
plots,
|
||||||
hlines,
|
hlines,
|
||||||
...(cloudColors ? { cloudColors } : {}),
|
...(cloudColors ? { cloudColors } : {}),
|
||||||
|
...(bandBackground ? { bandBackground } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type === 'SMA') {
|
if (type === 'SMA') {
|
||||||
@@ -1463,7 +1464,13 @@ function App() {
|
|||||||
isIndicatorSettingsTemplateId(settingsModalId ?? '')
|
isIndicatorSettingsTemplateId(settingsModalId ?? '')
|
||||||
|| updated.id.startsWith('template_');
|
|| updated.id.startsWith('template_');
|
||||||
saveParams(updated.type, updated.params);
|
saveParams(updated.type, updated.params);
|
||||||
saveVisual(updated.type, updated.plots, updated.hlines, updated.cloudColors);
|
saveVisual(
|
||||||
|
updated.type,
|
||||||
|
updated.plots,
|
||||||
|
updated.hlines,
|
||||||
|
updated.cloudColors,
|
||||||
|
updated.bandBackground,
|
||||||
|
);
|
||||||
|
|
||||||
const applyUpdate = (prev: IndicatorConfig[]): IndicatorConfig[] => {
|
const applyUpdate = (prev: IndicatorConfig[]): IndicatorConfig[] => {
|
||||||
if (fromTemplate) {
|
if (fromTemplate) {
|
||||||
@@ -1539,7 +1546,7 @@ function App() {
|
|||||||
applyChartIndicators(chartIndicators);
|
applyChartIndicators(chartIndicators);
|
||||||
allConfigs.forEach(ind => {
|
allConfigs.forEach(ind => {
|
||||||
saveParams(ind.type, ind.params);
|
saveParams(ind.type, ind.params);
|
||||||
saveVisual(ind.type, ind.plots, ind.hlines, ind.cloudColors);
|
saveVisual(ind.type, ind.plots, ind.hlines, ind.cloudColors, ind.bandBackground);
|
||||||
});
|
});
|
||||||
setShowBulkIndSettings(false);
|
setShowBulkIndSettings(false);
|
||||||
}, [applyChartIndicators, saveParams, saveVisual]);
|
}, [applyChartIndicators, saveParams, saveVisual]);
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
cfg = { ...fromConfig, id: newIndId(), type: def.type };
|
cfg = { ...fromConfig, id: newIndId(), type: def.type };
|
||||||
} else {
|
} else {
|
||||||
const params = getParams(type, def.defaultParams);
|
const params = getParams(type, def.defaultParams);
|
||||||
const { plots, hlines, cloudColors } = getVisualConfig(type, def.plots, def.hlines);
|
const { plots, hlines, cloudColors, bandBackground } = getVisualConfig(type, def.plots, def.hlines);
|
||||||
cfg = {
|
cfg = {
|
||||||
id: newIndId(),
|
id: newIndId(),
|
||||||
type: def.type,
|
type: def.type,
|
||||||
@@ -268,6 +268,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
plots,
|
plots,
|
||||||
hlines,
|
hlines,
|
||||||
...(cloudColors ? { cloudColors } : {}),
|
...(cloudColors ? { cloudColors } : {}),
|
||||||
|
...(bandBackground ? { bandBackground } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type === 'SMA') {
|
if (type === 'SMA') {
|
||||||
@@ -566,7 +567,13 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
// 파라미터 변경 DB 저장 → 백엔드 Ta4j 계산에 반영
|
// 파라미터 변경 DB 저장 → 백엔드 Ta4j 계산에 반영
|
||||||
saveParams(updated.type, updated.params);
|
saveParams(updated.type, updated.params);
|
||||||
// 시각 설정(색상·선굵기·수평선) 변경 DB 저장 → 새 지표 추가 시 기본값으로 사용
|
// 시각 설정(색상·선굵기·수평선) 변경 DB 저장 → 새 지표 추가 시 기본값으로 사용
|
||||||
saveVisual(updated.type, updated.plots, updated.hlines, updated.cloudColors);
|
saveVisual(
|
||||||
|
updated.type,
|
||||||
|
updated.plots,
|
||||||
|
updated.hlines,
|
||||||
|
updated.cloudColors,
|
||||||
|
updated.bandBackground,
|
||||||
|
);
|
||||||
}, [saveParams, saveVisual]);
|
}, [saveParams, saveVisual]);
|
||||||
|
|
||||||
const handleToggleHidden = useCallback((id: string) => {
|
const handleToggleHidden = useCallback((id: string) => {
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export const PlotSettingsRow: React.FC<{
|
|||||||
isHistogram && indicatorType === 'MACD' ? (
|
isHistogram && indicatorType === 'MACD' ? (
|
||||||
<div className="ism-hist-colors">
|
<div className="ism-hist-colors">
|
||||||
<div className="ism-hist-color-field">
|
<div className="ism-hist-color-field">
|
||||||
<span className="ism-style-label">상승</span>
|
<span className="ism-style-label">0선 위</span>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
value={resolveHistogramUpColor(plot)}
|
value={resolveHistogramUpColor(plot)}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
@@ -199,7 +199,7 @@ export const PlotSettingsRow: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ism-hist-color-field">
|
<div className="ism-hist-color-field">
|
||||||
<span className="ism-style-label">하락</span>
|
<span className="ism-style-label">0선 아래</span>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
value={resolveHistogramDownColor(plot)}
|
value={resolveHistogramDownColor(plot)}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ const IndicatorSettingsStyleSection: React.FC<IndicatorSettingsStyleSectionProps
|
|||||||
</div>
|
</div>
|
||||||
<div className="ism-style-row">
|
<div className="ism-style-row">
|
||||||
<div className="ism-plot-title-cell">
|
<div className="ism-plot-title-cell">
|
||||||
<span className="ism-plot-title ism-plot-title--sub">상승 막대</span>
|
<span className="ism-plot-title ism-plot-title--sub">0선 위 (양수)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ism-style-field ism-style-field--wide">
|
<div className="ism-style-field ism-style-field--wide">
|
||||||
<span className="ism-style-label">색상</span>
|
<span className="ism-style-label">색상</span>
|
||||||
@@ -225,7 +225,7 @@ const IndicatorSettingsStyleSection: React.FC<IndicatorSettingsStyleSectionProps
|
|||||||
</div>
|
</div>
|
||||||
<div className="ism-style-row">
|
<div className="ism-style-row">
|
||||||
<div className="ism-plot-title-cell">
|
<div className="ism-plot-title-cell">
|
||||||
<span className="ism-plot-title ism-plot-title--sub">하락 막대</span>
|
<span className="ism-plot-title ism-plot-title--sub">0선 아래 (음수)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ism-style-field ism-style-field--wide">
|
<div className="ism-style-field ism-style-field--wide">
|
||||||
<span className="ism-style-label">색상</span>
|
<span className="ism-style-label">색상</span>
|
||||||
|
|||||||
@@ -69,11 +69,6 @@ function getDomSubIndicatorLayouts(
|
|||||||
return subs;
|
return subs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 보조지표 1개 = 1 화면 슬롯 (병합 시 여러 item 이 한 슬롯) */
|
|
||||||
interface VisualSlot {
|
|
||||||
items: PaneItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PaneCapture {
|
interface PaneCapture {
|
||||||
dataUrl: string;
|
dataUrl: string;
|
||||||
cssWidth: number;
|
cssWidth: number;
|
||||||
@@ -220,80 +215,71 @@ function makeLabel(config: IndicatorConfig): string {
|
|||||||
return nums.length ? `${name} ${nums.join(', ')}` : name;
|
return nums.length ? `${name} ${nums.join(', ')}` : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVisualSlots(items: PaneItem[], indicators: IndicatorConfig[]): VisualSlot[] {
|
/** paneIndex ↔ LWC pane DOM 위치 (배열 순서·orphan pane 과 무관) */
|
||||||
const slots: VisualSlot[] = [];
|
function layoutForPaneIndex(
|
||||||
const hostSlotIdx = new Map<string, number>();
|
paneIndex: number,
|
||||||
|
layouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||||
for (const item of items) {
|
): { topY: number; height: number } | null {
|
||||||
const ind = indicators.find(i => i.id === item.id);
|
if (paneIndex < 2) return null;
|
||||||
if (ind?.mergedWith) {
|
const lay = layouts.find(l => l.paneIndex === paneIndex && l.height > MIN_SUB_PANE_HEIGHT);
|
||||||
const hostId = getPaneHostId(item.id, indicators);
|
return lay ? { topY: lay.topY, height: lay.height } : null;
|
||||||
const idx = hostSlotIdx.get(hostId);
|
|
||||||
if (idx !== undefined) slots[idx].items.push(item);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const idx = slots.length;
|
|
||||||
slots.push({ items: [item] });
|
|
||||||
hostSlotIdx.set(item.id, idx);
|
|
||||||
hostSlotIdx.set(getPaneHostId(item.id, indicators), idx);
|
|
||||||
}
|
|
||||||
return slots;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 각 슬롯(호스트 paneIndex) ↔ LWC pane 레이아웃 직접 매칭 — 배열 순서·orphan pane 무관 */
|
function applyPaneLayoutToItem(
|
||||||
function assignLayoutsByPaneIndex(
|
item: PaneItem,
|
||||||
slots: VisualSlot[],
|
layout: { topY: number; height: number },
|
||||||
|
overwrite = false,
|
||||||
|
): void {
|
||||||
|
if (!overwrite && item.layoutTopY != null) return;
|
||||||
|
item.layoutTopY = layout.topY;
|
||||||
|
item.layoutHeight = layout.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** paneIndex 기준으로만 좌표 보강 (지표 배열 순서와 화면 pane 순서가 달라도 안전) */
|
||||||
|
function fillMissingLayoutsByPaneIndex(
|
||||||
|
items: PaneItem[],
|
||||||
layouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
layouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||||
): void {
|
): void {
|
||||||
const byPane = new Map(layouts.map(l => [l.paneIndex, l]));
|
for (const item of items) {
|
||||||
for (const slot of slots) {
|
if (item.layoutTopY != null || item.paneIndex < 2) continue;
|
||||||
const host = slot.items[0];
|
const lay = layoutForPaneIndex(item.paneIndex, layouts);
|
||||||
if (!host || host.paneIndex < 2) continue;
|
if (lay) applyPaneLayoutToItem(item, lay);
|
||||||
const lay = byPane.get(host.paneIndex);
|
|
||||||
if (!lay || lay.height < MIN_SUB_PANE_HEIGHT) continue;
|
|
||||||
for (const item of slot.items) {
|
|
||||||
if (item.layoutTopY != null) continue;
|
|
||||||
item.layoutTopY = lay.topY;
|
|
||||||
item.layoutHeight = lay.height;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 화면 순서(위→아래)의 활성 sub-pane ↔ 슬롯 순서 — orphan pane index 와 분리 */
|
/** DOM 행 중 paneIndex 레이아웃 topY 와 가장 가까운 행 선택 */
|
||||||
function assignLayoutsBySubPaneOrder(
|
function fillMissingLayoutsFromDom(
|
||||||
slots: VisualSlot[],
|
items: PaneItem[],
|
||||||
subLayouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
containerEl: HTMLElement,
|
||||||
|
paneLayouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||||
): void {
|
): void {
|
||||||
for (let i = 0; i < slots.length && i < subLayouts.length; i++) {
|
const missing = items.filter(i => i.layoutTopY == null && i.paneIndex >= 2);
|
||||||
const lay = subLayouts[i];
|
if (missing.length === 0) return;
|
||||||
for (const item of slots[i].items) {
|
|
||||||
if (item.layoutTopY != null) continue;
|
|
||||||
item.layoutTopY = lay.topY;
|
|
||||||
item.layoutHeight = lay.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** paneIndex·orphan pane 으로 어긋난 좌표를 슬롯 순서 기준 sub-pane 위치로 보정 */
|
const domSubs = getDomSubIndicatorLayouts(containerEl, missing.length);
|
||||||
function reconcileLayoutsWithSubPanes(
|
if (domSubs.length === 0) return;
|
||||||
slots: VisualSlot[],
|
|
||||||
subLayouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
const usedDom = new Set<number>();
|
||||||
): void {
|
const sorted = [...missing].sort((a, b) => a.paneIndex - b.paneIndex);
|
||||||
const TOL = 24;
|
|
||||||
for (let i = 0; i < slots.length && i < subLayouts.length; i++) {
|
for (const item of sorted) {
|
||||||
const expected = subLayouts[i];
|
const ref = layoutForPaneIndex(item.paneIndex, paneLayouts);
|
||||||
for (const item of slots[i].items) {
|
let bestIdx = -1;
|
||||||
const cur = item.layoutTopY;
|
let bestDist = Infinity;
|
||||||
const curH = item.layoutHeight;
|
for (let i = 0; i < domSubs.length; i++) {
|
||||||
if (
|
if (usedDom.has(i)) continue;
|
||||||
cur == null
|
const dist = ref
|
||||||
|| Math.abs(cur - expected.topY) > TOL
|
? Math.abs(domSubs[i].topY - ref.topY)
|
||||||
|| (curH != null && Math.abs(curH - expected.height) > TOL)
|
: domSubs[i].topY;
|
||||||
) {
|
if (dist < bestDist) {
|
||||||
item.layoutTopY = expected.topY;
|
bestDist = dist;
|
||||||
item.layoutHeight = expected.height;
|
bestIdx = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (bestIdx < 0) continue;
|
||||||
|
if (ref && bestDist > 96) continue;
|
||||||
|
usedDom.add(bestIdx);
|
||||||
|
applyPaneLayoutToItem(item, domSubs[bestIdx]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,36 +319,24 @@ function buildPaneItems(
|
|||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const lay = manager.getIndicatorPaneScreenLayout(item.id);
|
const lay = manager.getIndicatorPaneScreenLayout(item.id);
|
||||||
if (lay) {
|
if (lay) applyPaneLayoutToItem(item, lay, true);
|
||||||
item.layoutTopY = lay.topY;
|
|
||||||
item.layoutHeight = lay.height;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const slots = buildVisualSlots(items, indicators);
|
const paneLayouts = manager.getPaneLayouts();
|
||||||
const subLayouts = manager.getIndicatorSubPaneLayouts();
|
fillMissingLayoutsByPaneIndex(items, paneLayouts);
|
||||||
assignLayoutsBySubPaneOrder(slots, subLayouts);
|
fillMissingLayoutsByPaneIndex(items, manager.getIndicatorSubPaneLayouts());
|
||||||
assignLayoutsByPaneIndex(slots, manager.getPaneLayouts());
|
|
||||||
if (subLayouts.length > 0) {
|
|
||||||
reconcileLayoutsWithSubPanes(slots, subLayouts);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (containerEl?.isConnected) {
|
if (containerEl?.isConnected) {
|
||||||
const missing = items.filter(i => i.layoutTopY == null);
|
fillMissingLayoutsFromDom(items, containerEl, paneLayouts);
|
||||||
if (missing.length > 0) {
|
|
||||||
const domSubs = getDomSubIndicatorLayouts(containerEl, slots.length);
|
|
||||||
for (let i = 0; i < slots.length && i < domSubs.length; i++) {
|
|
||||||
if (slots[i].items.every(it => it.layoutTopY != null)) continue;
|
|
||||||
const dom = domSubs[i];
|
|
||||||
for (const item of slots[i].items) {
|
|
||||||
if (item.layoutTopY != null) continue;
|
|
||||||
item.layoutTopY = dom.topY;
|
|
||||||
item.layoutHeight = dom.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items.sort((a, b) => {
|
||||||
|
const ay = a.layoutTopY ?? 1e9;
|
||||||
|
const by = b.layoutTopY ?? 1e9;
|
||||||
|
if (ay !== by) return ay - by;
|
||||||
|
return a.paneIndex - b.paneIndex;
|
||||||
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import { useIndicatorSettings } from '../hooks/useIndicatorSettings';
|
|||||||
import { findLogicNode, getIndicatorPeriodLabel } from '../utils/strategyFlowLayout';
|
import { findLogicNode, getIndicatorPeriodLabel } from '../utils/strategyFlowLayout';
|
||||||
import {
|
import {
|
||||||
decodeConditionForEditor,
|
decodeConditionForEditor,
|
||||||
|
alignBuySellStartCandleTypesForSave,
|
||||||
encodeConditionForSave,
|
encodeConditionForSave,
|
||||||
|
syncBuySellPrimaryStartCandleTypes,
|
||||||
mergeStartMetaForLoad,
|
mergeStartMetaForLoad,
|
||||||
addExtraStartSection,
|
addExtraStartSection,
|
||||||
hasMultipleStartSections,
|
hasMultipleStartSections,
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
type EditorConditionState,
|
type EditorConditionState,
|
||||||
} from '../utils/strategyConditionSerde';
|
} from '../utils/strategyConditionSerde';
|
||||||
import {
|
import {
|
||||||
|
START_NODE_ID,
|
||||||
defaultStartMeta,
|
defaultStartMeta,
|
||||||
type StartCombineOp,
|
type StartCombineOp,
|
||||||
} from '../utils/strategyStartNodes';
|
} from '../utils/strategyStartNodes';
|
||||||
@@ -393,9 +396,36 @@ export default function StrategyEditorPage({ theme }: Props) {
|
|||||||
}, [setCurrentRoot, setCurrentLayout, layoutStrategyKey, schedulePersistFlowLayout]);
|
}, [setCurrentRoot, setCurrentLayout, layoutStrategyKey, schedulePersistFlowLayout]);
|
||||||
|
|
||||||
const handleStartCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
|
const handleStartCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
|
||||||
|
if (startId === START_NODE_ID) {
|
||||||
|
const synced = syncBuySellPrimaryStartCandleTypes(buyEditorState, sellEditorState, candleTypes);
|
||||||
|
setBuyCondition(synced.buy.root);
|
||||||
|
setBuyLayout(prev => ({
|
||||||
|
...prev,
|
||||||
|
startMeta: synced.buy.startMeta,
|
||||||
|
extraStartIds: synced.buy.extraStartIds,
|
||||||
|
extraRoots: synced.buy.extraRoots,
|
||||||
|
startCombineOp: normalizeStartCombineOp(synced.buy.startCombineOp),
|
||||||
|
}));
|
||||||
|
setSellCondition(synced.sell.root);
|
||||||
|
setSellLayout(prev => ({
|
||||||
|
...prev,
|
||||||
|
startMeta: synced.sell.startMeta,
|
||||||
|
extraStartIds: synced.sell.extraStartIds,
|
||||||
|
extraRoots: synced.sell.extraRoots,
|
||||||
|
startCombineOp: normalizeStartCombineOp(synced.sell.startCombineOp),
|
||||||
|
}));
|
||||||
|
schedulePersistFlowLayout(layoutStrategyKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const state = signalTab === 'buy' ? buyEditorState : sellEditorState;
|
const state = signalTab === 'buy' ? buyEditorState : sellEditorState;
|
||||||
handleEditorStateChange(updateStartCandleTypes(state, startId, candleTypes));
|
handleEditorStateChange(updateStartCandleTypes(state, startId, candleTypes));
|
||||||
}, [signalTab, buyEditorState, sellEditorState, handleEditorStateChange]);
|
}, [
|
||||||
|
buyEditorState,
|
||||||
|
sellEditorState,
|
||||||
|
handleEditorStateChange,
|
||||||
|
layoutStrategyKey,
|
||||||
|
schedulePersistFlowLayout,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleStartCombineOpChange = useCallback((op: StartCombineOp) => {
|
const handleStartCombineOpChange = useCallback((op: StartCombineOp) => {
|
||||||
handleEditorStateChange(updateStartCombineOp(currentEditorState, op));
|
handleEditorStateChange(updateStartCombineOp(currentEditorState, op));
|
||||||
@@ -559,8 +589,9 @@ export default function StrategyEditorPage({ theme }: Props) {
|
|||||||
}
|
}
|
||||||
setSaveNameError(false);
|
setSaveNameError(false);
|
||||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||||
const encodedBuy = encodeConditionForSave(buyEditorState);
|
const aligned = alignBuySellStartCandleTypesForSave(buyEditorState, sellEditorState);
|
||||||
const encodedSell = encodeConditionForSave(sellEditorState);
|
const encodedBuy = encodeConditionForSave(aligned.buy);
|
||||||
|
const encodedSell = encodeConditionForSave(aligned.sell);
|
||||||
if (!encodedBuy && !encodedSell) { showSnack('조건을 최소 1개 추가하세요', false); return; }
|
if (!encodedBuy && !encodedSell) { showSnack('조건을 최소 1개 추가하세요', false); return; }
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
@@ -642,8 +673,9 @@ export default function StrategyEditorPage({ theme }: Props) {
|
|||||||
}, [resetFlowLayout, bumpLayoutSeed, signalTab]);
|
}, [resetFlowLayout, bumpLayoutSeed, signalTab]);
|
||||||
|
|
||||||
const handleExport = useCallback(() => {
|
const handleExport = useCallback(() => {
|
||||||
const encodedBuy = encodeConditionForSave(buyEditorState);
|
const aligned = alignBuySellStartCandleTypesForSave(buyEditorState, sellEditorState);
|
||||||
const encodedSell = encodeConditionForSave(sellEditorState);
|
const encodedBuy = encodeConditionForSave(aligned.buy);
|
||||||
|
const encodedSell = encodeConditionForSave(aligned.sell);
|
||||||
if (!encodedBuy && !encodedSell) {
|
if (!encodedBuy && !encodedSell) {
|
||||||
showSnack('내보낼 조건이 없습니다', false);
|
showSnack('내보낼 조건이 없습니다', false);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1430,7 +1430,7 @@ function sortedParamKey(inds: IndicatorConfig[]): string {
|
|||||||
*/
|
*/
|
||||||
function styleKey(inds: IndicatorConfig[]): string {
|
function styleKey(inds: IndicatorConfig[]): string {
|
||||||
return inds.map(i =>
|
return inds.map(i =>
|
||||||
`${i.id}|${i.hidden ? '1' : '0'}|${i.lastValueVisible === false ? '0' : '1'}|${(i.plots ?? []).map(p => `${p.color ?? ''}:${p.lineWidth ?? 1}:${p.lineStyle ?? 'solid'}:${p.histogramUpColor ?? ''}:${p.histogramDownColor ?? ''}`).join(',')}|${JSON.stringify(i.plotVisibility ?? {})}|${JSON.stringify((i.hlines ?? []).map(h => `${h.price}:${h.color}:${h.visible ?? true}:${h.lineStyle ?? 'dashed'}:${h.lineWidth ?? 1}`))}|${JSON.stringify(i.cloudColors ?? {})}`
|
`${i.id}|${i.hidden ? '1' : '0'}|${i.lastValueVisible === false ? '0' : '1'}|${(i.plots ?? []).map(p => `${p.color ?? ''}:${p.lineWidth ?? 1}:${p.lineStyle ?? 'solid'}:${p.histogramUpColor ?? ''}:${p.histogramDownColor ?? ''}`).join(',')}|${JSON.stringify(i.plotVisibility ?? {})}|${JSON.stringify((i.hlines ?? []).map(h => `${h.price}:${h.color}:${h.visible ?? true}:${h.lineStyle ?? 'dashed'}:${h.lineWidth ?? 1}`))}|${JSON.stringify(i.cloudColors ?? {})}|${JSON.stringify(i.hlinesBackground ?? {})}|${JSON.stringify(i.bandBackground ?? {})}`
|
||||||
).join(';');
|
).join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import {
|
|||||||
type IchimokuCloudColors,
|
type IchimokuCloudColors,
|
||||||
resolveIchimokuCloudColors,
|
resolveIchimokuCloudColors,
|
||||||
} from '../utils/ichimokuConfig';
|
} from '../utils/ichimokuConfig';
|
||||||
import type { IndicatorConfig } from '../types';
|
import type { HlinesBackground, IndicatorConfig } from '../types';
|
||||||
|
import { resolveBbBandBackground } from '../utils/bollingerConfig';
|
||||||
import {
|
import {
|
||||||
loadIndicatorSettings,
|
loadIndicatorSettings,
|
||||||
saveIndicatorSettings,
|
saveIndicatorSettings,
|
||||||
@@ -46,6 +47,16 @@ export interface IndicatorVisual {
|
|||||||
plots?: PlotDef[];
|
plots?: PlotDef[];
|
||||||
hlines?: HLineDef[];
|
hlines?: HLineDef[];
|
||||||
cloudColors?: IchimokuCloudColors;
|
cloudColors?: IchimokuCloudColors;
|
||||||
|
/** 볼린저밴드 어퍼~로우어 배경 (업비트 백그라운드 그리기) */
|
||||||
|
bandBackground?: HlinesBackground;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** getVisualConfig 반환 타입 */
|
||||||
|
export interface IndicatorVisualConfig {
|
||||||
|
plots: PlotDef[];
|
||||||
|
hlines: HLineDef[];
|
||||||
|
cloudColors?: IchimokuCloudColors;
|
||||||
|
bandBackground?: HlinesBackground;
|
||||||
}
|
}
|
||||||
type VisualCache = Record<string, IndicatorVisual>;
|
type VisualCache = Record<string, IndicatorVisual>;
|
||||||
|
|
||||||
@@ -204,12 +215,15 @@ export function useIndicatorSettings(sessionKey = 0) {
|
|||||||
hlines: cfg.hlines,
|
hlines: cfg.hlines,
|
||||||
};
|
};
|
||||||
if (cfg.cloudColors) visual.cloudColors = cfg.cloudColors;
|
if (cfg.cloudColors) visual.cloudColors = cfg.cloudColors;
|
||||||
|
if (cfg.type === 'BollingerBands' && cfg.bandBackground) {
|
||||||
|
visual.bandBackground = cfg.bandBackground;
|
||||||
|
}
|
||||||
return { type: cfg.type, visual };
|
return { type: cfg.type, visual };
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
visualEntries.map(({ type, visual }) =>
|
visualEntries.map(({ type, visual }) =>
|
||||||
saveIndicatorVisualSettings(type, visual as { plots?: unknown[]; hlines?: unknown[]; cloudColors?: IchimokuCloudColors }),
|
saveIndicatorVisualSettings(type, visual),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -238,8 +252,8 @@ export function useIndicatorSettings(sessionKey = 0) {
|
|||||||
(
|
(
|
||||||
type: string,
|
type: string,
|
||||||
defaultPlots?: PlotDef[],
|
defaultPlots?: PlotDef[],
|
||||||
defaultHlines?: HLineDef[]
|
defaultHlines?: HLineDef[],
|
||||||
): { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors } => {
|
): IndicatorVisualConfig => {
|
||||||
const saved = _visualCache?.[type];
|
const saved = _visualCache?.[type];
|
||||||
const def = getIndicatorDef(type);
|
const def = getIndicatorDef(type);
|
||||||
const registryPlots = defaultPlots ?? def?.plots ?? [];
|
const registryPlots = defaultPlots ?? def?.plots ?? [];
|
||||||
@@ -265,11 +279,16 @@ export function useIndicatorSettings(sessionKey = 0) {
|
|||||||
? resolveIchimokuCloudColors(saved?.cloudColors ?? DEFAULT_ICHIMOKU_CLOUD_COLORS)
|
? resolveIchimokuCloudColors(saved?.cloudColors ?? DEFAULT_ICHIMOKU_CLOUD_COLORS)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const bandBackground = type === 'BollingerBands'
|
||||||
|
? resolveBbBandBackground(saved?.bandBackground)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// 깊은 복사: 여러 슬롯이 같은 참조를 공유하지 않도록
|
// 깊은 복사: 여러 슬롯이 같은 참조를 공유하지 않도록
|
||||||
return {
|
return {
|
||||||
plots: plots.map(p => ({ ...p })),
|
plots: plots.map(p => ({ ...p })),
|
||||||
hlines: hlines.map(h => ({ ...h })),
|
hlines: hlines.map(h => ({ ...h })),
|
||||||
cloudColors: cloudColors ? { ...cloudColors } : undefined,
|
cloudColors: cloudColors ? { ...cloudColors } : undefined,
|
||||||
|
bandBackground: bandBackground ? { ...bandBackground } : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -285,15 +304,19 @@ export function useIndicatorSettings(sessionKey = 0) {
|
|||||||
plots?: PlotDef[],
|
plots?: PlotDef[],
|
||||||
hlines?: HLineDef[],
|
hlines?: HLineDef[],
|
||||||
cloudColors?: IchimokuCloudColors,
|
cloudColors?: IchimokuCloudColors,
|
||||||
|
bandBackground?: HlinesBackground,
|
||||||
) => {
|
) => {
|
||||||
const visual: IndicatorVisual = { plots, hlines };
|
const visual: IndicatorVisual = { plots, hlines };
|
||||||
if (cloudColors) visual.cloudColors = cloudColors;
|
if (cloudColors) visual.cloudColors = cloudColors;
|
||||||
|
if (type === 'BollingerBands') {
|
||||||
|
visual.bandBackground = bandBackground ?? resolveBbBandBackground(_visualCache?.[type]?.bandBackground);
|
||||||
|
}
|
||||||
if (_visualCache) {
|
if (_visualCache) {
|
||||||
_visualCache = { ..._visualCache, [type]: visual };
|
_visualCache = { ..._visualCache, [type]: visual };
|
||||||
} else {
|
} else {
|
||||||
_visualCache = { [type]: visual };
|
_visualCache = { [type]: visual };
|
||||||
}
|
}
|
||||||
saveIndicatorVisualSettings(type, visual as { plots?: unknown[]; hlines?: unknown[]; cloudColors?: IchimokuCloudColors }).catch(err =>
|
saveIndicatorVisualSettings(type, visual).catch(err =>
|
||||||
console.error(`[useIndicatorSettings] saveVisual failed for ${type}`, err)
|
console.error(`[useIndicatorSettings] saveVisual failed for ${type}`, err)
|
||||||
);
|
);
|
||||||
notifyIndicatorSettingsChanged();
|
notifyIndicatorSettingsChanged();
|
||||||
|
|||||||
@@ -147,11 +147,20 @@ function shouldShowBbBandFill(config: IndicatorConfig): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attachBbBandFill(entry: IndicatorEntry, config: IndicatorConfig): void {
|
function attachBbBandFill(entry: IndicatorEntry, config: IndicatorConfig): void {
|
||||||
|
if (!shouldShowBbBandFill(config)) {
|
||||||
|
detachBbBandFill(entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const basis = seriesByPlotId(entry, 'plot0');
|
const basis = seriesByPlotId(entry, 'plot0');
|
||||||
const upper = seriesByPlotId(entry, 'plot1');
|
const upper = seriesByPlotId(entry, 'plot1');
|
||||||
const lower = seriesByPlotId(entry, 'plot2');
|
const lower = seriesByPlotId(entry, 'plot2');
|
||||||
if (!basis || !upper || !lower || !shouldShowBbBandFill(config)) return;
|
if (!basis || !upper || !lower) return;
|
||||||
const bg = resolveBbBandBackground(config.bandBackground);
|
const bg = resolveBbBandBackground(config.bandBackground);
|
||||||
|
if (entry.bbFillPrimitive) {
|
||||||
|
entry.bbFillPrimitive.updateBackground(bg);
|
||||||
|
entry.bbFillPrimitive.requestRefresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
entry.bbFillPrimitive = new BollingerBandFillPrimitive(upper, lower, bg);
|
entry.bbFillPrimitive = new BollingerBandFillPrimitive(upper, lower, bg);
|
||||||
basis.attachPrimitive(entry.bbFillPrimitive);
|
basis.attachPrimitive(entry.bbFillPrimitive);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,10 +329,10 @@ export async function saveIndicatorSettings(
|
|||||||
* 앱 시작 시 호출 — 전역 시각 기본값을 DB 에서 가져온다.
|
* 앱 시작 시 호출 — 전역 시각 기본값을 DB 에서 가져온다.
|
||||||
*/
|
*/
|
||||||
export async function loadIndicatorVisualSettings(): Promise<
|
export async function loadIndicatorVisualSettings(): Promise<
|
||||||
Record<string, { plots?: unknown[]; hlines?: unknown[] }>
|
Record<string, { plots?: unknown[]; hlines?: unknown[]; cloudColors?: unknown; bandBackground?: unknown }>
|
||||||
> {
|
> {
|
||||||
return (
|
return (
|
||||||
(await request<Record<string, { plots?: unknown[]; hlines?: unknown[] }>>(
|
(await request<Record<string, { plots?: unknown[]; hlines?: unknown[]; cloudColors?: unknown; bandBackground?: unknown }>>(
|
||||||
'/indicator-settings/visual'
|
'/indicator-settings/visual'
|
||||||
)) ?? {}
|
)) ?? {}
|
||||||
);
|
);
|
||||||
@@ -347,7 +347,7 @@ export async function loadIndicatorVisualSettings(): Promise<
|
|||||||
*/
|
*/
|
||||||
export async function saveIndicatorVisualSettings(
|
export async function saveIndicatorVisualSettings(
|
||||||
indicatorType: string,
|
indicatorType: string,
|
||||||
visual: { plots?: unknown[]; hlines?: unknown[] }
|
visual: { plots?: unknown[]; hlines?: unknown[]; cloudColors?: unknown; bandBackground?: unknown },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await request(`/indicator-settings/${encodeURIComponent(indicatorType)}/visual`, {
|
await request(`/indicator-settings/${encodeURIComponent(indicatorType)}/visual`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ type GetVisual = (
|
|||||||
type: string,
|
type: string,
|
||||||
defaultPlots?: PlotDef[],
|
defaultPlots?: PlotDef[],
|
||||||
defaultHlines?: HLineDef[],
|
defaultHlines?: HLineDef[],
|
||||||
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors };
|
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors; bandBackground?: import('../types').HlinesBackground };
|
||||||
|
|
||||||
/** 탭 전체 추가 — 기존 차트 지표를 제거하고 types 목록만 새로 구성 */
|
/** 탭 전체 추가 — 기존 차트 지표를 제거하고 types 목록만 새로 구성 */
|
||||||
export function replaceIndicatorConfigsFromTypes(
|
export function replaceIndicatorConfigsFromTypes(
|
||||||
@@ -181,7 +181,7 @@ export function buildIndicatorConfigsFromTypes(
|
|||||||
seen.add(type);
|
seen.add(type);
|
||||||
|
|
||||||
const params = getParams(type, def.defaultParams);
|
const params = getParams(type, def.defaultParams);
|
||||||
const { plots, hlines, cloudColors } = getVisualConfig(type, def.plots, def.hlines);
|
const { plots, hlines, cloudColors, bandBackground } = getVisualConfig(type, def.plots, def.hlines);
|
||||||
let cfg: IndicatorConfig = {
|
let cfg: IndicatorConfig = {
|
||||||
id: newId(),
|
id: newId(),
|
||||||
type: def.type,
|
type: def.type,
|
||||||
@@ -189,6 +189,7 @@ export function buildIndicatorConfigsFromTypes(
|
|||||||
plots,
|
plots,
|
||||||
hlines,
|
hlines,
|
||||||
...(cloudColors ? { cloudColors } : {}),
|
...(cloudColors ? { cloudColors } : {}),
|
||||||
|
...(bandBackground ? { bandBackground } : {}),
|
||||||
};
|
};
|
||||||
if (type === 'SMA') {
|
if (type === 'SMA') {
|
||||||
cfg = normalizeSmaConfig({
|
cfg = normalizeSmaConfig({
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function initializeIndicatorConfigForEditor(
|
|||||||
type: string,
|
type: string,
|
||||||
defaultPlots: PlotDef[],
|
defaultPlots: PlotDef[],
|
||||||
defaultHlines?: HLineDef[],
|
defaultHlines?: HLineDef[],
|
||||||
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors },
|
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors; bandBackground?: HlinesBackground },
|
||||||
): IndicatorConfig {
|
): IndicatorConfig {
|
||||||
const def = getIndicatorDef(raw.type);
|
const def = getIndicatorDef(raw.type);
|
||||||
if (!def) return enrichIndicatorConfig(raw);
|
if (!def) return enrichIndicatorConfig(raw);
|
||||||
@@ -70,6 +70,7 @@ export function initializeIndicatorConfigForEditor(
|
|||||||
plots: raw.plots?.length ? raw.plots : visual?.plots,
|
plots: raw.plots?.length ? raw.plots : visual?.plots,
|
||||||
hlines: raw.hlines?.length ? raw.hlines : visual?.hlines,
|
hlines: raw.hlines?.length ? raw.hlines : visual?.hlines,
|
||||||
cloudColors: raw.cloudColors ?? visual?.cloudColors,
|
cloudColors: raw.cloudColors ?? visual?.cloudColors,
|
||||||
|
bandBackground: raw.bandBackground ?? visual?.bandBackground,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,12 +68,27 @@ export function resolveHistogramDownColor(plot: PlotDef): string {
|
|||||||
return plot.histogramDownColor ?? DEFAULT_HIST_DOWN;
|
return plot.histogramDownColor ?? DEFAULT_HIST_DOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** MACD 등 histogram 막대 색 (상승·하락/음수 구분) */
|
/** MACD 히스토그램 — histogramUp/DownColor 로 0선 위·아래 색 분리 */
|
||||||
|
export function isMacdDualHistogramPlot(plot: PlotDef): boolean {
|
||||||
|
return plot.type === 'histogram'
|
||||||
|
&& plot.histogramUpColor != null
|
||||||
|
&& plot.histogramDownColor != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* histogram 막대 색.
|
||||||
|
* - MACD(상·하 색 지정): 0선 기준 — 양수=상승색, 음수=하락색 (전봴 대비 혼합 없음)
|
||||||
|
* - 기타: 전봴 대비 방향 + 음수 영역 하락색
|
||||||
|
*/
|
||||||
export function histogramBarColor(
|
export function histogramBarColor(
|
||||||
value: number,
|
value: number,
|
||||||
prev: number | null | undefined,
|
prev: number | null | undefined,
|
||||||
plot: PlotDef,
|
plot: PlotDef,
|
||||||
): string {
|
): string {
|
||||||
|
if (isMacdDualHistogramPlot(plot)) {
|
||||||
|
const raw = value < 0 ? resolveHistogramDownColor(plot) : resolveHistogramUpColor(plot);
|
||||||
|
return withHistogramAlpha(raw);
|
||||||
|
}
|
||||||
const isDown = prev != null && !isNaN(prev) && value < prev;
|
const isDown = prev != null && !isNaN(prev) && value < prev;
|
||||||
const isNeg = value < 0;
|
const isNeg = value < 0;
|
||||||
const raw = (isDown || isNeg) ? resolveHistogramDownColor(plot) : resolveHistogramUpColor(plot);
|
const raw = (isDown || isNeg) ? resolveHistogramDownColor(plot) : resolveHistogramUpColor(plot);
|
||||||
|
|||||||
@@ -399,6 +399,31 @@ export function collectTimeframesFromEditorState(state: EditorConditionState): s
|
|||||||
return [...set];
|
return [...set];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 매수·매도 START(기본) 평가 분봉을 동일하게 맞춤 — 한쪽만 3m·5m 체크 시 다른 쪽 1m 잔존 방지 */
|
||||||
|
export function syncBuySellPrimaryStartCandleTypes(
|
||||||
|
buy: EditorConditionState,
|
||||||
|
sell: EditorConditionState,
|
||||||
|
candleTypes: string[],
|
||||||
|
): { buy: EditorConditionState; sell: EditorConditionState } {
|
||||||
|
const types = normalizeCandleTypesList(candleTypes);
|
||||||
|
return {
|
||||||
|
buy: updateStartCandleTypes(buy, START_NODE_ID, types),
|
||||||
|
sell: updateStartCandleTypes(sell, START_NODE_ID, types),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 저장 직전 — 양쪽 START 분봉 합집합을 각 탭에 반영 (매수만 설정한 경우 매도 DSL도 동기화) */
|
||||||
|
export function alignBuySellStartCandleTypesForSave(
|
||||||
|
buy: EditorConditionState,
|
||||||
|
sell: EditorConditionState,
|
||||||
|
): { buy: EditorConditionState; sell: EditorConditionState } {
|
||||||
|
const merged = normalizeCandleTypesList([
|
||||||
|
...getStartCandleTypes(buy.startMeta[START_NODE_ID]),
|
||||||
|
...getStartCandleTypes(sell.startMeta[START_NODE_ID]),
|
||||||
|
]);
|
||||||
|
return syncBuySellPrimaryStartCandleTypes(buy, sell, merged);
|
||||||
|
}
|
||||||
|
|
||||||
/** 저장된 DSL에서 Logic Expression용 분기 목록 */
|
/** 저장된 DSL에서 Logic Expression용 분기 목록 */
|
||||||
export function collectDslBranches(dsl: LogicNode | null): ConditionBranch[] {
|
export function collectDslBranches(dsl: LogicNode | null): ConditionBranch[] {
|
||||||
return collectEditorBranches(decodeConditionForEditor(dsl));
|
return collectEditorBranches(decodeConditionForEditor(dsl));
|
||||||
|
|||||||
Reference in New Issue
Block a user