일목-BB 수정

This commit is contained in:
Macbook
2026-06-23 21:24:37 +09:00
parent 3e26373bfe
commit 5d90d1a74e
13 changed files with 226 additions and 239 deletions
@@ -1143,7 +1143,7 @@ public class StrategyDslToTa4jAdapter {
}
/**
* 일목 후행스팬(현재 종가) vs N봉 전 볼린저밴드 — 상향/하향 돌파·위/아래 유지.
* N봉 전 후행스팬(종가) vs N봉 전 볼린저밴드 — 동일 시점(현재봉 기준 N봉 전) 비교.
* cond.params.chikouDisplacement (기본 26), rightField: UPPER_BAND | MIDDLE_BAND | LOWER_BAND
*/
private Rule buildIchimokuBbRule(
@@ -1175,13 +1175,15 @@ public class StrategyDslToTa4jAdapter {
ClosePriceIndicator close = new ClosePriceIndicator(series);
Indicator<Num> bbLine = resolveField(bandField, "BOLLINGER", bbParams, series, -1, -1, cond, ctx);
// 후행스팬 = 해당 봉 종가. N봉 전 시점의 후행스팬·BB를 같은 인덱스(t-N)에서 비교.
Indicator<Num> priorLagging = new PreviousValueIndicator(close, d);
Indicator<Num> priorBb = new PreviousValueIndicator(bbLine, d);
Rule core = switch (condType) {
case "CROSS_UP" -> buildCrossUpRule(close, priorBb);
case "CROSS_DOWN" -> buildCrossDownRule(close, priorBb);
case "GT", "GTE" -> new OverIndicatorRule(close, priorBb);
case "LT", "LTE" -> new UnderIndicatorRule(close, priorBb);
case "CROSS_UP" -> buildCrossUpRule(priorLagging, priorBb);
case "CROSS_DOWN" -> buildCrossDownRule(priorLagging, priorBb);
case "GT", "GTE" -> new OverIndicatorRule(priorLagging, priorBb);
case "LT", "LTE" -> new UnderIndicatorRule(priorLagging, priorBb);
default -> {
log.warn("[Adapter] ICHIMOKU_BB 미지원 conditionType: {}", condType);
yield new BooleanRule(false);
@@ -0,0 +1,100 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.Rule;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.bollinger.BollingerBandsUpperIndicator;
import org.ta4j.core.indicators.helpers.ClosePriceIndicator;
import org.ta4j.core.indicators.helpers.PreviousValueIndicator;
import org.ta4j.core.num.DoubleNumFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/**
* ICHIMOKU_BB — N봉 전 후행스팬(종가) vs N봉 전 BB 동일 시점 비교.
*/
class IchimokuBbRuleTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
private StrategyDslToTa4jAdapter adapter;
private BarSeries series;
@BeforeEach
void setUp() {
adapter = new StrategyDslToTa4jAdapter();
series = buildSeries(80);
}
@Test
void ichimokuBb_gt_usesSameOffsetForLaggingAndBb() throws Exception {
JsonNode cond = MAPPER.readTree("""
{
"type": "CONDITION",
"condition": {
"indicatorType": "ICHIMOKU_BB",
"conditionType": "GT",
"leftField": "LAGGING_SPAN",
"rightField": "UPPER_BAND",
"candleRange": 1,
"params": { "chikouDisplacement": 5 }
}
}
""");
int evalIndex = 40;
int d = 5;
Rule rule = adapter.toRule(cond, series, Map.of());
ClosePriceIndicator close = new ClosePriceIndicator(series);
SMAIndicator sma = new SMAIndicator(close, 20);
BollingerBandsUpperIndicator upper = new BollingerBandsUpperIndicator(
sma, series.numFactory().numOf(2));
PreviousValueIndicator priorClose = new PreviousValueIndicator(close, d);
PreviousValueIndicator priorUpper = new PreviousValueIndicator(upper, d);
boolean expectedSameOffset = priorClose.getValue(evalIndex)
.isGreaterThan(priorUpper.getValue(evalIndex));
boolean wrongCurrentClose = close.getValue(evalIndex)
.isGreaterThan(priorUpper.getValue(evalIndex));
assertNotEquals(expectedSameOffset, wrongCurrentClose,
"테스트 시계열에서 현재 종가 vs N봉 전 BB와 N봉 전 종가 vs N봉 전 BB가 달라야 함");
assertEquals(expectedSameOffset, rule.isSatisfied(evalIndex, null));
}
private static BarSeries buildSeries(int bars) {
BarSeries s = new BaseBarSeriesBuilder()
.withName("ichimoku-bb-test")
.withNumFactory(DoubleNumFactory.getInstance())
.withBarBuilderFactory(new TimeBarBuilderFactory())
.build();
TimeBarBuilderFactory factory = new TimeBarBuilderFactory();
Instant base = Instant.parse("2024-01-01T00:00:00Z");
Duration period = Duration.ofMinutes(3);
double price = 100.0;
for (int i = 0; i < bars; i++) {
double drift = Math.sin(i * 0.35) * 3.0 + (i > 35 ? (i - 35) * 0.4 : 0);
double c = price + drift;
factory.createBarBuilder(s)
.timePeriod(period)
.endTime(base.plus(period.multipliedBy(i + 1L)))
.openPrice(c).highPrice(c + 1.0).lowPrice(c - 1.0).closePrice(c)
.volume(1000)
.add();
price = c;
}
return s;
}
}
@@ -78,11 +78,6 @@ import {
import {
buildIchimokuBbTree,
isIchimokuBbPaletteValue,
isIchimokuBbPairRoot,
setIchimokuBbPairConfig,
patchIchimokuBbPairInTree,
inferIchimokuBbConfig,
type IchimokuBbPairConfig,
} from '../utils/ichimokuBbStrategy';
import {
buildStableStrategyTree,
@@ -105,7 +100,6 @@ import {
import StochPairNodeSettings from './strategyEditor/StochPairNodeSettings';
import PriceExtremePairNodeSettings from './strategyEditor/PriceExtremePairNodeSettings';
import Inflection33PairNodeSettings from './strategyEditor/Inflection33PairNodeSettings';
import IchimokuBbPairNodeSettings from './strategyEditor/IchimokuBbPairNodeSettings';
import StableStrategyPairNodeSettings from './strategyEditor/StableStrategyPairNodeSettings';
import {
emptySignalFlowLayout,
@@ -741,36 +735,6 @@ export default function StrategyEditorPage({
handleOrphansChange, handleRootChange, handleExtraRootsChange,
]);
const applyIchimokuBbConfig = useCallback((pairNodeId: string, patch: Partial<IchimokuBbPairConfig>) => {
if (currentOrphans.some(o => o.id === pairNodeId)) {
handleOrphansChange(currentOrphans.map(o => (
o.id === pairNodeId ? setIchimokuBbPairConfig(o, patch, DEF) : o
)));
return;
}
if (currentRoot) {
const patched = patchIchimokuBbPairInTree(currentRoot, pairNodeId, patch, DEF);
if (patched && patched !== currentRoot) {
handleRootChange(patched);
return;
}
}
for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
if (!branch) continue;
const patched = patchIchimokuBbPairInTree(branch, pairNodeId, patch, DEF);
if (patched && patched !== branch) {
handleExtraRootsChange({
...(currentLayout.extraRoots ?? {}),
[startId]: patched,
});
return;
}
}
}, [
currentRoot, currentOrphans, currentLayout.extraRoots, DEF,
handleOrphansChange, handleRootChange, handleExtraRootsChange,
]);
const applyStableStrategyFilterLevel = useCallback((pairNodeId: string, filterLevel: StableStrategyFilterLevel) => {
if (currentOrphans.some(o => o.id === pairNodeId)) {
handleOrphansChange(currentOrphans.map(o => (
@@ -2186,17 +2150,6 @@ export default function StrategyEditorPage({
/>
</div>
)}
{selectedLogicNode && isIchimokuBbPairRoot(selectedLogicNode) && selectedLogicNode.ichimokuBbPair && (
<div className="se-node-config-bar se-node-config-bar--ichimoku-bb">
<span className="se-node-config-label">×</span>
<IchimokuBbPairNodeSettings
variant="inline"
config={inferIchimokuBbConfig(selectedLogicNode)}
onChange={patch => applyIchimokuBbConfig(selectedLogicNode.id, patch)}
onClose={() => {}}
/>
</div>
)}
{selectedLogicNode && isStableStrategyPairRoot(selectedLogicNode) && selectedLogicNode.stableStrategyPair && (
<div className="se-node-config-bar se-node-config-bar--stable-strategy">
<span className="se-node-config-label"> </span>
@@ -26,6 +26,10 @@ import {
import { compositeDisplayName, compositeElementLabel, COMPOSITE_INDICATOR_ITEMS, switchCompositeIndicatorType } from '../../utils/compositeIndicators';
import { STRATEGY_CANDLE_TYPE_OPTIONS } from '../../utils/strategyStartNodes';
import ComboNumberInput from './ComboNumberInput';
import {
getChikouDisplacement,
setChikouDisplacement,
} from '../../utils/ichimokuBbStrategy';
interface Props {
condition: ConditionDSL;
@@ -154,6 +158,17 @@ export default function ConditionNodeSettings({
onChange={p => onChange(setConditionValuePeriod(condition, p, def))}
/>
</label>
) : condition.indicatorType === 'ICHIMOKU_BB' ? (
<label className="se-flow-settings-field">
<span> N봉</span>
<ComboNumberInput
value={getChikouDisplacement(condition)}
options={[26, 9, 13, 20, 33, 52, 60]}
min={1}
max={120}
onChange={n => onChange(setChikouDisplacement(condition, n))}
/>
</label>
) : null}
{showThreshold && chartRef != null && (
<>
@@ -181,6 +196,11 @@ export default function ConditionNodeSettings({
{condition.composite && (
<p className="se-flow-settings-hint">{compositeDisplayName(condition.indicatorType)}</p>
)}
{condition.indicatorType === 'ICHIMOKU_BB' && (
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
N봉 () .
</p>
)}
</div>
);
}
@@ -23,10 +23,8 @@ import {
import {
isIchimokuBbPairRoot,
ichimokuBbDisplayName,
inferIchimokuBbConfig,
} from '../../utils/ichimokuBbStrategy';
import Inflection33PairNodeSettings from './Inflection33PairNodeSettings';
import IchimokuBbPairNodeSettings from './IchimokuBbPairNodeSettings';
import {
isStableStrategyPairRoot,
stableStrategyDisplayName,
@@ -324,21 +322,6 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
<span className="se-flow-gate-stoch-pair-title">
{ichimokuBbDisplayName(node.ichimokuBbPair.mode, node.ichimokuBbPair.chikouDisplacement)}
</span>
<button
type="button"
className="se-flow-settings"
title="일목×볼린저 설정"
aria-label="일목×볼린저 설정"
onClick={e => {
e.stopPropagation();
setSettingsOpen(v => !v);
}}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
</button>
</div>
)}
{isPriceExtremePair && node.priceExtremePair && (
@@ -419,13 +402,6 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
onClose={() => setSettingsOpen(false)}
/>
)}
{settingsOpen && isIchimokuBbPair && node.ichimokuBbPair && (
<IchimokuBbPairNodeSettings
config={inferIchimokuBbConfig(node)}
onChange={patch => d.onUpdateIchimokuBbConfig?.(id, patch)}
onClose={() => setSettingsOpen(false)}
/>
)}
{settingsOpen && isPriceExtremePair && node.priceExtremePair && (
<PriceExtremePairNodeSettings
mode={node.priceExtremePair.mode}
@@ -1,128 +0,0 @@
import React, { useEffect, useRef } from 'react';
import {
ICHIMOKU_BB_BAND_OPTIONS,
ICHIMOKU_BB_CONDITION_OPTIONS,
ichimokuBbDisplayName,
ichimokuBbSummaryText,
type IchimokuBbBand,
type IchimokuBbConditionType,
type IchimokuBbPairConfig,
} from '../../utils/ichimokuBbStrategy';
interface Props {
config: IchimokuBbPairConfig;
onChange: (patch: Partial<IchimokuBbPairConfig>) => void;
onClose: () => void;
popoverClassName?: string;
variant?: 'popover' | 'inline';
}
export default function IchimokuBbPairNodeSettings({
config,
onChange,
onClose,
popoverClassName = 'se-flow-settings-pop',
variant = 'popover',
}: Props) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (variant === 'inline') return;
const onDoc = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
};
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [onClose, variant]);
const body = (
<>
<label className="se-flow-settings-field se-flow-settings-field--readonly">
<span></span>
<output className="se-flow-settings-readout">
{ichimokuBbDisplayName(config.mode, config.chikouDisplacement)}
</output>
</label>
<label className="se-flow-settings-field">
<span> N봉</span>
<input
type="number"
className="se-combo-num-input"
min={1}
max={120}
value={config.chikouDisplacement}
onChange={e => {
const n = parseInt(e.target.value, 10);
if (Number.isFinite(n) && n > 0) onChange({ chikouDisplacement: n });
}}
/>
</label>
<label className="se-flow-settings-field">
<span> </span>
<select
className="se-combo-num-select sp-cond-sel"
value={config.bbBand}
onChange={e => onChange({ bbBand: e.target.value as IchimokuBbBand })}
>
{ICHIMOKU_BB_BAND_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</label>
<label className="se-flow-settings-field">
<span></span>
<select
className="se-combo-num-select sp-cond-sel"
value={config.conditionType}
onChange={e => onChange({ conditionType: e.target.value as IchimokuBbConditionType })}
>
{ICHIMOKU_BB_CONDITION_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</label>
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
() N봉 .
</p>
<p className="se-flow-settings-hint se-flow-settings-hint--muted">
{ichimokuBbSummaryText(config)}
</p>
</>
);
if (variant === 'inline') {
return (
<div className="se-ichimoku-bb-inline-settings" onClick={e => e.stopPropagation()}>
{body}
</div>
);
}
return (
<div
ref={ref}
className={popoverClassName}
role="dialog"
aria-label="일목×볼린저 설정"
onClick={e => e.stopPropagation()}
>
<div className="se-flow-settings-head">
<span>×</span>
<button
type="button"
className="se-flow-settings-close"
aria-label="닫기"
title="닫기"
onClick={onClose}
>
×
</button>
</div>
{body}
</div>
);
}
@@ -58,8 +58,7 @@ import {
type Inflection33FilterLevel,
} from '../../utils/inflection33Strategy';
import {
setIchimokuBbPairConfig,
type IchimokuBbPairConfig,
syncIchimokuBbPairsInTree,
} from '../../utils/ichimokuBbStrategy';
import {
setStableStrategyPairFilterLevel,
@@ -722,21 +721,24 @@ function StrategyEditorCanvasInner({
const handleUpdateCondition = useCallback((id: string, condition: ConditionDSL) => {
const next = normalizeConditionForPersistence(condition);
const patchTree = (tree: LogicNode) => syncIchimokuBbPairsInTree(
updateNode(tree, id, n => ({ ...n, condition: next })),
)!;
if (isOrphanNode(orphans, id)) {
onOrphansChange(orphans.map(o => (
o.id === id ? { ...o, condition: next } : o
o.id === id ? patchTree(o) : o
)));
return;
}
if (root && findNodeInTree(root, id)) {
onChange(updateNode(root, id, n => ({ ...n, condition: next })));
onChange(patchTree(root));
return;
}
for (const [sid, branch] of Object.entries(extraRoots)) {
if (branch && findNodeInTree(branch, id)) {
onExtraRootsChange?.({
...extraRoots,
[sid]: updateNode(branch, id, n => ({ ...n, condition: next })),
[sid]: patchTree(branch),
});
return;
}
@@ -875,27 +877,6 @@ function StrategyEditorCanvasInner({
}
}, [root, extraRoots, orphans, def, onChange, onOrphansChange, onExtraRootsChange]);
const handleUpdateIchimokuBbConfig = useCallback((id: string, patch: Partial<IchimokuBbPairConfig>) => {
const patchPair = (n: LogicNode) => setIchimokuBbPairConfig(n, patch, def);
if (isOrphanNode(orphans, id)) {
onOrphansChange(orphans.map(o => (o.id === id ? patchPair(o) : o)));
return;
}
if (root && findNodeInTree(root, id)) {
onChange(updateNode(root, id, patchPair));
return;
}
for (const [sid, branch] of Object.entries(extraRoots)) {
if (branch && findNodeInTree(branch, id)) {
onExtraRootsChange?.({
...extraRoots,
[sid]: updateNode(branch, id, patchPair),
});
return;
}
}
}, [root, extraRoots, orphans, def, onChange, onOrphansChange, onExtraRootsChange]);
const handleUpdateStableStrategyFilterLevel = useCallback((id: string, filterLevel: StableStrategyFilterLevel) => {
const patchPair = (n: LogicNode) => setStableStrategyPairFilterLevel(n, filterLevel, def);
if (isOrphanNode(orphans, id)) {
@@ -924,7 +905,6 @@ function StrategyEditorCanvasInner({
onUpdateStochPairSecondary: handleUpdateStochPairSecondary,
onUpdatePriceExtremeFilterLevel: handleUpdatePriceExtremeFilterLevel,
onUpdateInflection33FilterLevel: handleUpdateInflection33FilterLevel,
onUpdateIchimokuBbConfig: handleUpdateIchimokuBbConfig,
onUpdateStableStrategyFilterLevel: handleUpdateStableStrategyFilterLevel,
onChangeLogicGateType: handleChangeLogicGateType,
onDropTarget: handleDropTarget,
@@ -933,7 +913,7 @@ function StrategyEditorCanvasInner({
onStartCandleTypesChange: handleStartCandleTypesChange,
onDeleteStart: handleDeleteStart,
}), [
handleDelete, handleUpdateCondition, handleReplaceLogicNode, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateIchimokuBbConfig, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType,
handleDelete, handleUpdateCondition, handleReplaceLogicNode, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType,
handleDropTarget, handleDragOverTarget, handleDragLeaveTarget,
handleStartCandleTypesChange, handleDeleteStart,
]);
@@ -19,6 +19,7 @@ import {
setStochPairSecondary,
stochPairDisplayName,
} from '../../utils/stochOverboughtPair';
import { syncIchimokuBbPairsInTree } from '../../utils/ichimokuBbStrategy';
import ConditionNodeSettings from './ConditionNodeSettings';
import ConditionPresetPicker from './ConditionPresetPicker';
import StochPairNodeSettings from './StochPairNodeSettings';
@@ -101,7 +102,9 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
: 'NOT (반대)';
const handleCondChange = (c: NonNullable<LogicNode['condition']>) => {
onUpdate(prev => updateNode(prev, node.id, n => ({ ...n, condition: c })));
onUpdate(prev => syncIchimokuBbPairsInTree(
updateNode(prev, node.id, n => ({ ...n, condition: c })),
)!);
};
const stochPairRoot = stochPairForest && node.condition
+1
View File
@@ -446,6 +446,7 @@ export function initConditionPeriods(
export function hasNodeSettings(cond: ConditionDSL): boolean {
return usesValuePeriodField(cond)
|| hasEditableThreshold(cond)
|| cond.indicatorType === 'ICHIMOKU_BB'
|| !!cond.composite;
}
+63 -5
View File
@@ -1,8 +1,8 @@
/**
* ×
*
* () vs N봉(chikouDisplacement) ··
* / ·/
* N봉(chikouDisplacement)
* () vs ·· · ·/
*/
import type { ConditionDSL, LogicNode } from './strategyTypes';
import { initConditionPeriodsInherit, type IndicatorPeriodDef } from './conditionPeriods';
@@ -136,15 +136,16 @@ export function ichimokuBbDisplayName(mode: 'buy' | 'sell', displacement = DEFAU
}
export function ichimokuBbSummaryText(config: IchimokuBbPairConfig): string {
return `후행스팬 ${config.chikouDisplacement}봉 전 ${bandLabel(config.bbBand)} ${conditionLabel(config.conditionType)}`;
const n = config.chikouDisplacement;
return `${n}봉 전 후행스팬(종가) — ${n}봉 전 ${bandLabel(config.bbBand)} ${conditionLabel(config.conditionType)}`;
}
export function ichimokuBbPaletteDesc(value: string): string {
if (isIchimokuBbBuyPaletteValue(value)) {
return `후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 상한선 상향 돌파 — 밴드·N·조건 변경 가능`;
return `${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 상한선 상향 돌파 — 밴드·N·조건 변경 가능`;
}
if (isIchimokuBbSellPaletteValue(value)) {
return `후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 하한선 하향 돌파 — 밴드·N·조건 변경 가능`;
return `${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 하한선 하향 돌파 — 밴드·N·조건 변경 가능`;
}
return '';
}
@@ -196,3 +197,60 @@ export function patchIchimokuBbPairInTree(
});
return changed ? { ...root, children } : root;
}
export function getChikouDisplacement(cond: ConditionDSL): number {
const n = Number(cond.params?.chikouDisplacement);
return Number.isFinite(n) && n > 0 ? n : DEFAULT_CHIKOU_DISPLACEMENT;
}
export function setChikouDisplacement(cond: ConditionDSL, displacement: number): ConditionDSL {
const n = Math.max(1, Math.round(displacement));
return {
...cond,
params: { ...(cond.params ?? {}), chikouDisplacement: n },
};
}
export function ichimokuBbPairMetaFromCondition(
mode: 'buy' | 'sell',
cond: ConditionDSL,
): Omit<IchimokuBbPairConfig, 'mode'> {
const defaults = mode === 'buy' ? DEFAULT_BUY : DEFAULT_SELL;
return {
chikouDisplacement: getChikouDisplacement(cond),
bbBand: (cond.rightField as IchimokuBbBand) ?? defaults.bbBand,
conditionType: (cond.conditionType as IchimokuBbConditionType) ?? defaults.conditionType,
};
}
/** 자식 CONDITION 변경 시 게이트 ichimokuBbPair 메타 동기화 */
export function syncIchimokuBbPairFromChild(node: LogicNode): LogicNode {
if (!isIchimokuBbPairRoot(node) || !node.ichimokuBbPair) return node;
const cond = node.children?.[0]?.condition;
if (!cond || cond.indicatorType !== 'ICHIMOKU_BB') return node;
const nextMeta: IchimokuBbPairConfig = {
mode: node.ichimokuBbPair.mode,
...ichimokuBbPairMetaFromCondition(node.ichimokuBbPair.mode, cond),
};
const prev = node.ichimokuBbPair;
if (
prev.chikouDisplacement === nextMeta.chikouDisplacement
&& prev.bbBand === nextMeta.bbBand
&& prev.conditionType === nextMeta.conditionType
) {
return node;
}
return { ...node, ichimokuBbPair: nextMeta };
}
export function syncIchimokuBbPairsInTree(root: LogicNode | null): LogicNode | null {
if (!root) return null;
let node = root;
if (node.children?.length) {
const children = node.children.map(c => syncIchimokuBbPairsInTree(c)!);
if (children.some((c, i) => c !== node.children![i])) {
node = { ...node, children };
}
}
return syncIchimokuBbPairFromChild(node);
}
@@ -51,6 +51,7 @@ import {
ichimokuBbDisplayName,
ichimokuBbSummaryText,
inferIchimokuBbConfig,
getChikouDisplacement,
} from '../utils/ichimokuBbStrategy';
import {
buildStableStrategyTree,
@@ -782,6 +783,17 @@ function buildFieldOptsForSlot(
];
return slot === 'right' ? [NONE, ...lines] : lines;
}
case 'ICHIMOKU_BB': {
const chikouN = getChikouDisplacement(cond ?? { indicatorType: 'ICHIMOKU_BB' } as ConditionDSL);
if (slot === 'left') {
return [{ value: 'LAGGING_SPAN', label: `${chikouN}봉 전 후행스팬(종가)` }];
}
return [
{ value: 'UPPER_BAND', label: `${chikouN}봉 전 상한선(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'MIDDLE_BAND', label: `${chikouN}봉 전 중앙값(${DEF.bbPeriod}일)` },
{ value: 'LOWER_BAND', label: `${chikouN}봉 전 하한선(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
];
}
default:
return slot === 'right' ? [NONE] : [];
}
@@ -839,6 +851,9 @@ const getDefaultConditionFields = (ind: string, signalType: 'buy'|'sell', DEF: D
NEW_HIGH: { l:'CLOSE_PRICE', r: nhPriorField(9) },
NEW_LOW: { l:'CLOSE_PRICE', r: nlPriorField(9) },
ICHIMOKU: { l:'CONVERSION_LINE', r:'BASE_LINE' },
ICHIMOKU_BB: signalType === 'buy'
? { l: 'LAGGING_SPAN', r: 'UPPER_BAND' }
: { l: 'LAGGING_SPAN', r: 'LOWER_BAND' },
};
return map[ind] ?? { l:'NONE', r:'NONE' };
};
@@ -231,7 +231,14 @@ export function resetPaletteItems(kind: PaletteItemKind): PaletteItem[] {
/** 전략편집기·전략빌더에 표시할 지표명 (우측 팔레트 label 우선) */
const STRATEGY_INDICATOR_TYPE_ALIASES: Record<string, string> = {};
const STATIC_INDICATOR_DISPLAY_NAMES: Record<string, string> = {
ICHIMOKU_BB: '일목×BB',
};
export function getStrategyIndicatorDisplayName(indicatorType: string): string {
if (STATIC_INDICATOR_DISPLAY_NAMES[indicatorType]) {
return STATIC_INDICATOR_DISPLAY_NAMES[indicatorType];
}
const resolved = STRATEGY_INDICATOR_TYPE_ALIASES[indicatorType] ?? indicatorType;
for (const kind of ['auxiliary', 'composite'] as const) {
+1 -1
View File
@@ -198,7 +198,7 @@ export const LEFT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
{ value: 'LAGGING_SPAN', label: '후행스팬' },
],
ICHIMOKU_BB: [
{ value: 'LAGGING_SPAN', label: '후행스팬' },
{ value: 'LAGGING_SPAN', label: 'N봉 전 후행스팬(종가)' },
],
};