일목-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
|
* cond.params.chikouDisplacement (기본 26), rightField: UPPER_BAND | MIDDLE_BAND | LOWER_BAND
|
||||||
*/
|
*/
|
||||||
private Rule buildIchimokuBbRule(
|
private Rule buildIchimokuBbRule(
|
||||||
@@ -1175,13 +1175,15 @@ public class StrategyDslToTa4jAdapter {
|
|||||||
|
|
||||||
ClosePriceIndicator close = new ClosePriceIndicator(series);
|
ClosePriceIndicator close = new ClosePriceIndicator(series);
|
||||||
Indicator<Num> bbLine = resolveField(bandField, "BOLLINGER", bbParams, series, -1, -1, cond, ctx);
|
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);
|
Indicator<Num> priorBb = new PreviousValueIndicator(bbLine, d);
|
||||||
|
|
||||||
Rule core = switch (condType) {
|
Rule core = switch (condType) {
|
||||||
case "CROSS_UP" -> buildCrossUpRule(close, priorBb);
|
case "CROSS_UP" -> buildCrossUpRule(priorLagging, priorBb);
|
||||||
case "CROSS_DOWN" -> buildCrossDownRule(close, priorBb);
|
case "CROSS_DOWN" -> buildCrossDownRule(priorLagging, priorBb);
|
||||||
case "GT", "GTE" -> new OverIndicatorRule(close, priorBb);
|
case "GT", "GTE" -> new OverIndicatorRule(priorLagging, priorBb);
|
||||||
case "LT", "LTE" -> new UnderIndicatorRule(close, priorBb);
|
case "LT", "LTE" -> new UnderIndicatorRule(priorLagging, priorBb);
|
||||||
default -> {
|
default -> {
|
||||||
log.warn("[Adapter] ICHIMOKU_BB 미지원 conditionType: {}", condType);
|
log.warn("[Adapter] ICHIMOKU_BB 미지원 conditionType: {}", condType);
|
||||||
yield new BooleanRule(false);
|
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 {
|
import {
|
||||||
buildIchimokuBbTree,
|
buildIchimokuBbTree,
|
||||||
isIchimokuBbPaletteValue,
|
isIchimokuBbPaletteValue,
|
||||||
isIchimokuBbPairRoot,
|
|
||||||
setIchimokuBbPairConfig,
|
|
||||||
patchIchimokuBbPairInTree,
|
|
||||||
inferIchimokuBbConfig,
|
|
||||||
type IchimokuBbPairConfig,
|
|
||||||
} from '../utils/ichimokuBbStrategy';
|
} from '../utils/ichimokuBbStrategy';
|
||||||
import {
|
import {
|
||||||
buildStableStrategyTree,
|
buildStableStrategyTree,
|
||||||
@@ -105,7 +100,6 @@ import {
|
|||||||
import StochPairNodeSettings from './strategyEditor/StochPairNodeSettings';
|
import StochPairNodeSettings from './strategyEditor/StochPairNodeSettings';
|
||||||
import PriceExtremePairNodeSettings from './strategyEditor/PriceExtremePairNodeSettings';
|
import PriceExtremePairNodeSettings from './strategyEditor/PriceExtremePairNodeSettings';
|
||||||
import Inflection33PairNodeSettings from './strategyEditor/Inflection33PairNodeSettings';
|
import Inflection33PairNodeSettings from './strategyEditor/Inflection33PairNodeSettings';
|
||||||
import IchimokuBbPairNodeSettings from './strategyEditor/IchimokuBbPairNodeSettings';
|
|
||||||
import StableStrategyPairNodeSettings from './strategyEditor/StableStrategyPairNodeSettings';
|
import StableStrategyPairNodeSettings from './strategyEditor/StableStrategyPairNodeSettings';
|
||||||
import {
|
import {
|
||||||
emptySignalFlowLayout,
|
emptySignalFlowLayout,
|
||||||
@@ -741,36 +735,6 @@ export default function StrategyEditorPage({
|
|||||||
handleOrphansChange, handleRootChange, handleExtraRootsChange,
|
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) => {
|
const applyStableStrategyFilterLevel = useCallback((pairNodeId: string, filterLevel: StableStrategyFilterLevel) => {
|
||||||
if (currentOrphans.some(o => o.id === pairNodeId)) {
|
if (currentOrphans.some(o => o.id === pairNodeId)) {
|
||||||
handleOrphansChange(currentOrphans.map(o => (
|
handleOrphansChange(currentOrphans.map(o => (
|
||||||
@@ -2186,17 +2150,6 @@ export default function StrategyEditorPage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{selectedLogicNode && isStableStrategyPairRoot(selectedLogicNode) && selectedLogicNode.stableStrategyPair && (
|
||||||
<div className="se-node-config-bar se-node-config-bar--stable-strategy">
|
<div className="se-node-config-bar se-node-config-bar--stable-strategy">
|
||||||
<span className="se-node-config-label">추천 전략 필터</span>
|
<span className="se-node-config-label">추천 전략 필터</span>
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ import {
|
|||||||
import { compositeDisplayName, compositeElementLabel, COMPOSITE_INDICATOR_ITEMS, switchCompositeIndicatorType } from '../../utils/compositeIndicators';
|
import { compositeDisplayName, compositeElementLabel, COMPOSITE_INDICATOR_ITEMS, switchCompositeIndicatorType } from '../../utils/compositeIndicators';
|
||||||
import { STRATEGY_CANDLE_TYPE_OPTIONS } from '../../utils/strategyStartNodes';
|
import { STRATEGY_CANDLE_TYPE_OPTIONS } from '../../utils/strategyStartNodes';
|
||||||
import ComboNumberInput from './ComboNumberInput';
|
import ComboNumberInput from './ComboNumberInput';
|
||||||
|
import {
|
||||||
|
getChikouDisplacement,
|
||||||
|
setChikouDisplacement,
|
||||||
|
} from '../../utils/ichimokuBbStrategy';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
condition: ConditionDSL;
|
condition: ConditionDSL;
|
||||||
@@ -154,6 +158,17 @@ export default function ConditionNodeSettings({
|
|||||||
onChange={p => onChange(setConditionValuePeriod(condition, p, def))}
|
onChange={p => onChange(setConditionValuePeriod(condition, p, def))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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}
|
) : null}
|
||||||
{showThreshold && chartRef != null && (
|
{showThreshold && chartRef != null && (
|
||||||
<>
|
<>
|
||||||
@@ -181,6 +196,11 @@ export default function ConditionNodeSettings({
|
|||||||
{condition.composite && (
|
{condition.composite && (
|
||||||
<p className="se-flow-settings-hint">{compositeDisplayName(condition.indicatorType)}</p>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
isIchimokuBbPairRoot,
|
isIchimokuBbPairRoot,
|
||||||
ichimokuBbDisplayName,
|
ichimokuBbDisplayName,
|
||||||
inferIchimokuBbConfig,
|
|
||||||
} from '../../utils/ichimokuBbStrategy';
|
} from '../../utils/ichimokuBbStrategy';
|
||||||
import Inflection33PairNodeSettings from './Inflection33PairNodeSettings';
|
import Inflection33PairNodeSettings from './Inflection33PairNodeSettings';
|
||||||
import IchimokuBbPairNodeSettings from './IchimokuBbPairNodeSettings';
|
|
||||||
import {
|
import {
|
||||||
isStableStrategyPairRoot,
|
isStableStrategyPairRoot,
|
||||||
stableStrategyDisplayName,
|
stableStrategyDisplayName,
|
||||||
@@ -324,21 +322,6 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
|
|||||||
<span className="se-flow-gate-stoch-pair-title">
|
<span className="se-flow-gate-stoch-pair-title">
|
||||||
{ichimokuBbDisplayName(node.ichimokuBbPair.mode, node.ichimokuBbPair.chikouDisplacement)}
|
{ichimokuBbDisplayName(node.ichimokuBbPair.mode, node.ichimokuBbPair.chikouDisplacement)}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isPriceExtremePair && node.priceExtremePair && (
|
{isPriceExtremePair && node.priceExtremePair && (
|
||||||
@@ -419,13 +402,6 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
|
|||||||
onClose={() => setSettingsOpen(false)}
|
onClose={() => setSettingsOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{settingsOpen && isIchimokuBbPair && node.ichimokuBbPair && (
|
|
||||||
<IchimokuBbPairNodeSettings
|
|
||||||
config={inferIchimokuBbConfig(node)}
|
|
||||||
onChange={patch => d.onUpdateIchimokuBbConfig?.(id, patch)}
|
|
||||||
onClose={() => setSettingsOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{settingsOpen && isPriceExtremePair && node.priceExtremePair && (
|
{settingsOpen && isPriceExtremePair && node.priceExtremePair && (
|
||||||
<PriceExtremePairNodeSettings
|
<PriceExtremePairNodeSettings
|
||||||
mode={node.priceExtremePair.mode}
|
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,
|
type Inflection33FilterLevel,
|
||||||
} from '../../utils/inflection33Strategy';
|
} from '../../utils/inflection33Strategy';
|
||||||
import {
|
import {
|
||||||
setIchimokuBbPairConfig,
|
syncIchimokuBbPairsInTree,
|
||||||
type IchimokuBbPairConfig,
|
|
||||||
} from '../../utils/ichimokuBbStrategy';
|
} from '../../utils/ichimokuBbStrategy';
|
||||||
import {
|
import {
|
||||||
setStableStrategyPairFilterLevel,
|
setStableStrategyPairFilterLevel,
|
||||||
@@ -722,21 +721,24 @@ function StrategyEditorCanvasInner({
|
|||||||
|
|
||||||
const handleUpdateCondition = useCallback((id: string, condition: ConditionDSL) => {
|
const handleUpdateCondition = useCallback((id: string, condition: ConditionDSL) => {
|
||||||
const next = normalizeConditionForPersistence(condition);
|
const next = normalizeConditionForPersistence(condition);
|
||||||
|
const patchTree = (tree: LogicNode) => syncIchimokuBbPairsInTree(
|
||||||
|
updateNode(tree, id, n => ({ ...n, condition: next })),
|
||||||
|
)!;
|
||||||
if (isOrphanNode(orphans, id)) {
|
if (isOrphanNode(orphans, id)) {
|
||||||
onOrphansChange(orphans.map(o => (
|
onOrphansChange(orphans.map(o => (
|
||||||
o.id === id ? { ...o, condition: next } : o
|
o.id === id ? patchTree(o) : o
|
||||||
)));
|
)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (root && findNodeInTree(root, id)) {
|
if (root && findNodeInTree(root, id)) {
|
||||||
onChange(updateNode(root, id, n => ({ ...n, condition: next })));
|
onChange(patchTree(root));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const [sid, branch] of Object.entries(extraRoots)) {
|
for (const [sid, branch] of Object.entries(extraRoots)) {
|
||||||
if (branch && findNodeInTree(branch, id)) {
|
if (branch && findNodeInTree(branch, id)) {
|
||||||
onExtraRootsChange?.({
|
onExtraRootsChange?.({
|
||||||
...extraRoots,
|
...extraRoots,
|
||||||
[sid]: updateNode(branch, id, n => ({ ...n, condition: next })),
|
[sid]: patchTree(branch),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -875,27 +877,6 @@ function StrategyEditorCanvasInner({
|
|||||||
}
|
}
|
||||||
}, [root, extraRoots, orphans, def, onChange, onOrphansChange, onExtraRootsChange]);
|
}, [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 handleUpdateStableStrategyFilterLevel = useCallback((id: string, filterLevel: StableStrategyFilterLevel) => {
|
||||||
const patchPair = (n: LogicNode) => setStableStrategyPairFilterLevel(n, filterLevel, def);
|
const patchPair = (n: LogicNode) => setStableStrategyPairFilterLevel(n, filterLevel, def);
|
||||||
if (isOrphanNode(orphans, id)) {
|
if (isOrphanNode(orphans, id)) {
|
||||||
@@ -924,7 +905,6 @@ function StrategyEditorCanvasInner({
|
|||||||
onUpdateStochPairSecondary: handleUpdateStochPairSecondary,
|
onUpdateStochPairSecondary: handleUpdateStochPairSecondary,
|
||||||
onUpdatePriceExtremeFilterLevel: handleUpdatePriceExtremeFilterLevel,
|
onUpdatePriceExtremeFilterLevel: handleUpdatePriceExtremeFilterLevel,
|
||||||
onUpdateInflection33FilterLevel: handleUpdateInflection33FilterLevel,
|
onUpdateInflection33FilterLevel: handleUpdateInflection33FilterLevel,
|
||||||
onUpdateIchimokuBbConfig: handleUpdateIchimokuBbConfig,
|
|
||||||
onUpdateStableStrategyFilterLevel: handleUpdateStableStrategyFilterLevel,
|
onUpdateStableStrategyFilterLevel: handleUpdateStableStrategyFilterLevel,
|
||||||
onChangeLogicGateType: handleChangeLogicGateType,
|
onChangeLogicGateType: handleChangeLogicGateType,
|
||||||
onDropTarget: handleDropTarget,
|
onDropTarget: handleDropTarget,
|
||||||
@@ -933,7 +913,7 @@ function StrategyEditorCanvasInner({
|
|||||||
onStartCandleTypesChange: handleStartCandleTypesChange,
|
onStartCandleTypesChange: handleStartCandleTypesChange,
|
||||||
onDeleteStart: handleDeleteStart,
|
onDeleteStart: handleDeleteStart,
|
||||||
}), [
|
}), [
|
||||||
handleDelete, handleUpdateCondition, handleReplaceLogicNode, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateIchimokuBbConfig, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType,
|
handleDelete, handleUpdateCondition, handleReplaceLogicNode, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType,
|
||||||
handleDropTarget, handleDragOverTarget, handleDragLeaveTarget,
|
handleDropTarget, handleDragOverTarget, handleDragLeaveTarget,
|
||||||
handleStartCandleTypesChange, handleDeleteStart,
|
handleStartCandleTypesChange, handleDeleteStart,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
setStochPairSecondary,
|
setStochPairSecondary,
|
||||||
stochPairDisplayName,
|
stochPairDisplayName,
|
||||||
} from '../../utils/stochOverboughtPair';
|
} from '../../utils/stochOverboughtPair';
|
||||||
|
import { syncIchimokuBbPairsInTree } from '../../utils/ichimokuBbStrategy';
|
||||||
import ConditionNodeSettings from './ConditionNodeSettings';
|
import ConditionNodeSettings from './ConditionNodeSettings';
|
||||||
import ConditionPresetPicker from './ConditionPresetPicker';
|
import ConditionPresetPicker from './ConditionPresetPicker';
|
||||||
import StochPairNodeSettings from './StochPairNodeSettings';
|
import StochPairNodeSettings from './StochPairNodeSettings';
|
||||||
@@ -101,7 +102,9 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
|
|||||||
: 'NOT (반대)';
|
: 'NOT (반대)';
|
||||||
|
|
||||||
const handleCondChange = (c: NonNullable<LogicNode['condition']>) => {
|
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
|
const stochPairRoot = stochPairForest && node.condition
|
||||||
|
|||||||
@@ -446,6 +446,7 @@ export function initConditionPeriods(
|
|||||||
export function hasNodeSettings(cond: ConditionDSL): boolean {
|
export function hasNodeSettings(cond: ConditionDSL): boolean {
|
||||||
return usesValuePeriodField(cond)
|
return usesValuePeriodField(cond)
|
||||||
|| hasEditableThreshold(cond)
|
|| hasEditableThreshold(cond)
|
||||||
|
|| cond.indicatorType === 'ICHIMOKU_BB'
|
||||||
|| !!cond.composite;
|
|| !!cond.composite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* 일목 후행스팬 × 볼린저밴드 복합 전략
|
* 일목 후행스팬 × 볼린저밴드 복합 전략
|
||||||
*
|
*
|
||||||
* 현재 후행스팬(종가) vs N봉(chikouDisplacement) 전 볼린저 상한선·중앙값·하한선
|
* 현재 봉 기준 N봉(chikouDisplacement) 전 시점에서
|
||||||
* — 상향/하향 돌파·위/아래 유지 조건
|
* 후행스팬(종가) vs 볼린저 상한선·중앙값·하한선 — 상·하향 돌파·위/아래 유지
|
||||||
*/
|
*/
|
||||||
import type { ConditionDSL, LogicNode } from './strategyTypes';
|
import type { ConditionDSL, LogicNode } from './strategyTypes';
|
||||||
import { initConditionPeriodsInherit, type IndicatorPeriodDef } from './conditionPeriods';
|
import { initConditionPeriodsInherit, type IndicatorPeriodDef } from './conditionPeriods';
|
||||||
@@ -136,15 +136,16 @@ export function ichimokuBbDisplayName(mode: 'buy' | 'sell', displacement = DEFAU
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ichimokuBbSummaryText(config: IchimokuBbPairConfig): string {
|
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 {
|
export function ichimokuBbPaletteDesc(value: string): string {
|
||||||
if (isIchimokuBbBuyPaletteValue(value)) {
|
if (isIchimokuBbBuyPaletteValue(value)) {
|
||||||
return `후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 상한선 상향 돌파 — 밴드·N·조건 변경 가능`;
|
return `${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 상한선 상향 돌파 — 밴드·N·조건 변경 가능`;
|
||||||
}
|
}
|
||||||
if (isIchimokuBbSellPaletteValue(value)) {
|
if (isIchimokuBbSellPaletteValue(value)) {
|
||||||
return `후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 하한선 하향 돌파 — 밴드·N·조건 변경 가능`;
|
return `${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 하한선 하향 돌파 — 밴드·N·조건 변경 가능`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -196,3 +197,60 @@ export function patchIchimokuBbPairInTree(
|
|||||||
});
|
});
|
||||||
return changed ? { ...root, children } : root;
|
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,
|
ichimokuBbDisplayName,
|
||||||
ichimokuBbSummaryText,
|
ichimokuBbSummaryText,
|
||||||
inferIchimokuBbConfig,
|
inferIchimokuBbConfig,
|
||||||
|
getChikouDisplacement,
|
||||||
} from '../utils/ichimokuBbStrategy';
|
} from '../utils/ichimokuBbStrategy';
|
||||||
import {
|
import {
|
||||||
buildStableStrategyTree,
|
buildStableStrategyTree,
|
||||||
@@ -782,6 +783,17 @@ function buildFieldOptsForSlot(
|
|||||||
];
|
];
|
||||||
return slot === 'right' ? [NONE, ...lines] : lines;
|
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:
|
default:
|
||||||
return slot === 'right' ? [NONE] : [];
|
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_HIGH: { l:'CLOSE_PRICE', r: nhPriorField(9) },
|
||||||
NEW_LOW: { l:'CLOSE_PRICE', r: nlPriorField(9) },
|
NEW_LOW: { l:'CLOSE_PRICE', r: nlPriorField(9) },
|
||||||
ICHIMOKU: { l:'CONVERSION_LINE', r:'BASE_LINE' },
|
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' };
|
return map[ind] ?? { l:'NONE', r:'NONE' };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -231,7 +231,14 @@ export function resetPaletteItems(kind: PaletteItemKind): PaletteItem[] {
|
|||||||
/** 전략편집기·전략빌더에 표시할 지표명 (우측 팔레트 label 우선) */
|
/** 전략편집기·전략빌더에 표시할 지표명 (우측 팔레트 label 우선) */
|
||||||
const STRATEGY_INDICATOR_TYPE_ALIASES: Record<string, string> = {};
|
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 {
|
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;
|
const resolved = STRATEGY_INDICATOR_TYPE_ALIASES[indicatorType] ?? indicatorType;
|
||||||
|
|
||||||
for (const kind of ['auxiliary', 'composite'] as const) {
|
for (const kind of ['auxiliary', 'composite'] as const) {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export const LEFT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
|
|||||||
{ value: 'LAGGING_SPAN', label: '후행스팬' },
|
{ value: 'LAGGING_SPAN', label: '후행스팬' },
|
||||||
],
|
],
|
||||||
ICHIMOKU_BB: [
|
ICHIMOKU_BB: [
|
||||||
{ value: 'LAGGING_SPAN', label: '후행스팬' },
|
{ value: 'LAGGING_SPAN', label: 'N봉 전 후행스팬(종가)' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user