일목-BB 수정
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -446,6 +446,7 @@ export function initConditionPeriods(
|
||||
export function hasNodeSettings(cond: ConditionDSL): boolean {
|
||||
return usesValuePeriodField(cond)
|
||||
|| hasEditableThreshold(cond)
|
||||
|| cond.indicatorType === 'ICHIMOKU_BB'
|
||||
|| !!cond.composite;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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봉 전 후행스팬(종가)' },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user