일목균형표 전략 추가

This commit is contained in:
Macbook
2026-06-23 20:42:04 +09:00
parent d352b01b3c
commit 3e26373bfe
18 changed files with 674 additions and 31 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ export const DEFAULT_BB_BAND_BACKGROUND = {
color: '#42A5F528',
} as const;
const BB_PLOT_TITLES = ['중앙값', '어퍼', '로우어'] as const;
const BB_PLOT_TITLES = ['중앙값', '상한선', '하한선'] as const;
const BB_PLOT_IDS = ['plot0', 'plot1', 'plot2'] as const;
const BB_DEFAULT_COLORS = ['#FF9800', '#42A5F5', '#42A5F5'] as const;
@@ -34,7 +34,7 @@ export function resolveBbBandBackground(
};
}
/** 업비트 플롯 정의 병합 (중앙값·어퍼·로우어, 기본 색) */
/** 업비트 플롯 정의 병합 (중앙값·상한선·하한선, 기본 색) */
export function mergeBbPlots(
saved: import('./indicatorRegistry').PlotDef[] | undefined,
defaults: import('./indicatorRegistry').PlotDef[],
+2 -2
View File
@@ -29,8 +29,8 @@ export type ChartCustomOverlaySelection = {
export const BB_CUSTOM_PLOT_IDS = ['plot0', 'plot1', 'plot2'] as const;
export const BB_CUSTOM_LABELS: Record<(typeof BB_CUSTOM_PLOT_IDS)[number], string> = {
plot0: '중앙값',
plot1: '어퍼',
plot2: '로우어',
plot1: '상한선',
plot2: '하한선',
};
export function createDefaultChartCustomOverlaySelection(): ChartCustomOverlaySelection {
+198
View File
@@ -0,0 +1,198 @@
/**
* 일목 후행스팬 × 볼린저밴드 복합 전략
*
* 현재 후행스팬(종가) vs N봉(chikouDisplacement) 전 볼린저 상한선·중앙값·하한선
* — 상향/하향 돌파·위/아래 유지 조건
*/
import type { ConditionDSL, LogicNode } from './strategyTypes';
import { initConditionPeriodsInherit, type IndicatorPeriodDef } from './conditionPeriods';
import { genId } from './strategyNodeIds';
export const ICHIMOKU_BB_BUY_VALUE = 'ICHIMOKU_BB_BUY';
export const ICHIMOKU_BB_SELL_VALUE = 'ICHIMOKU_BB_SELL';
export const DEFAULT_CHIKOU_DISPLACEMENT = 26;
export type IchimokuBbBand = 'UPPER_BAND' | 'MIDDLE_BAND' | 'LOWER_BAND';
export type IchimokuBbConditionType = 'CROSS_UP' | 'CROSS_DOWN' | 'GT' | 'LT';
export interface IchimokuBbPairConfig {
mode: 'buy' | 'sell';
chikouDisplacement: number;
bbBand: IchimokuBbBand;
conditionType: IchimokuBbConditionType;
}
export const ICHIMOKU_BB_BAND_OPTIONS: { value: IchimokuBbBand; label: string }[] = [
{ value: 'UPPER_BAND', label: '상한선' },
{ value: 'MIDDLE_BAND', label: '중앙값' },
{ value: 'LOWER_BAND', label: '하한선' },
];
export const ICHIMOKU_BB_CONDITION_OPTIONS: { value: IchimokuBbConditionType; label: string }[] = [
{ value: 'CROSS_UP', label: '상향 돌파' },
{ value: 'CROSS_DOWN', label: '하향 돌파' },
{ value: 'GT', label: '위 유지(>)' },
{ value: 'LT', label: '아래 유지(<)' },
];
const DEFAULT_BUY: Omit<IchimokuBbPairConfig, 'mode'> = {
chikouDisplacement: DEFAULT_CHIKOU_DISPLACEMENT,
bbBand: 'UPPER_BAND',
conditionType: 'CROSS_UP',
};
const DEFAULT_SELL: Omit<IchimokuBbPairConfig, 'mode'> = {
chikouDisplacement: DEFAULT_CHIKOU_DISPLACEMENT,
bbBand: 'LOWER_BAND',
conditionType: 'CROSS_DOWN',
};
export function isIchimokuBbBuyPaletteValue(value: string): boolean {
return value === ICHIMOKU_BB_BUY_VALUE;
}
export function isIchimokuBbSellPaletteValue(value: string): boolean {
return value === ICHIMOKU_BB_SELL_VALUE;
}
export function isIchimokuBbPaletteValue(value: string): boolean {
return isIchimokuBbBuyPaletteValue(value) || isIchimokuBbSellPaletteValue(value);
}
export function isIchimokuBbPairRoot(node: LogicNode): boolean {
return !!node.ichimokuBbPair?.mode && (node.type === 'AND' || node.type === 'OR');
}
function bandLabel(band: IchimokuBbBand): string {
return ICHIMOKU_BB_BAND_OPTIONS.find(o => o.value === band)?.label ?? band;
}
function conditionLabel(ct: IchimokuBbConditionType): string {
return ICHIMOKU_BB_CONDITION_OPTIONS.find(o => o.value === ct)?.label ?? ct;
}
function makeIchimokuBbCondition(
def: IndicatorPeriodDef,
config: Omit<IchimokuBbPairConfig, 'mode'>,
): LogicNode {
const base: ConditionDSL = {
indicatorType: 'ICHIMOKU_BB',
conditionType: config.conditionType,
leftField: 'LAGGING_SPAN',
rightField: config.bbBand,
candleRange: 1,
valuePeriodOverride: false,
thresholdOverride: false,
rightPeriodOverride: false,
params: {
chikouDisplacement: config.chikouDisplacement,
},
};
return {
id: genId(),
type: 'CONDITION',
condition: initConditionPeriodsInherit('ICHIMOKU_BB', base, def),
};
}
function wrapPairRoot(
mode: 'buy' | 'sell',
config: Omit<IchimokuBbPairConfig, 'mode'>,
def: IndicatorPeriodDef,
): LogicNode {
return {
id: genId(),
type: 'AND',
ichimokuBbPair: { mode, ...config },
children: [makeIchimokuBbCondition(def, config)],
};
}
export function buildIchimokuBbBuyTree(
def: IndicatorPeriodDef,
config: Partial<Omit<IchimokuBbPairConfig, 'mode'>> = {},
): LogicNode {
return wrapPairRoot('buy', { ...DEFAULT_BUY, ...config }, def);
}
export function buildIchimokuBbSellTree(
def: IndicatorPeriodDef,
config: Partial<Omit<IchimokuBbPairConfig, 'mode'>> = {},
): LogicNode {
return wrapPairRoot('sell', { ...DEFAULT_SELL, ...config }, def);
}
export function buildIchimokuBbTree(value: string, def: IndicatorPeriodDef): LogicNode | null {
if (isIchimokuBbBuyPaletteValue(value)) return buildIchimokuBbBuyTree(def);
if (isIchimokuBbSellPaletteValue(value)) return buildIchimokuBbSellTree(def);
return null;
}
export function ichimokuBbDisplayName(mode: 'buy' | 'sell', displacement = DEFAULT_CHIKOU_DISPLACEMENT): string {
return mode === 'buy'
? `일목×BB 매수 (N=${displacement})`
: `일목×BB 매도 (N=${displacement})`;
}
export function ichimokuBbSummaryText(config: IchimokuBbPairConfig): string {
return `후행스팬 ${config.chikouDisplacement}봉 전 ${bandLabel(config.bbBand)} ${conditionLabel(config.conditionType)}`;
}
export function ichimokuBbPaletteDesc(value: string): string {
if (isIchimokuBbBuyPaletteValue(value)) {
return `후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 상한선 상향 돌파 — 밴드·N·조건 변경 가능`;
}
if (isIchimokuBbSellPaletteValue(value)) {
return `후행스팬(종가)이 ${DEFAULT_CHIKOU_DISPLACEMENT}봉 전 볼린저 하한선 하향 돌파 — 밴드·N·조건 변경 가능`;
}
return '';
}
export function inferIchimokuBbConfig(node: LogicNode): IchimokuBbPairConfig {
if (node.ichimokuBbPair) return node.ichimokuBbPair;
const cond = node.children?.[0]?.condition;
const displacement = Number(cond?.params?.chikouDisplacement) || DEFAULT_CHIKOU_DISPLACEMENT;
const bbBand = (cond?.rightField as IchimokuBbBand) ?? DEFAULT_BUY.bbBand;
const conditionType = (cond?.conditionType as IchimokuBbConditionType) ?? DEFAULT_BUY.conditionType;
return {
mode: 'buy',
chikouDisplacement: displacement,
bbBand,
conditionType,
};
}
export function setIchimokuBbPairConfig(
node: LogicNode,
patch: Partial<IchimokuBbPairConfig>,
def: IndicatorPeriodDef,
): LogicNode {
if (!isIchimokuBbPairRoot(node) || !node.ichimokuBbPair) return node;
const next: IchimokuBbPairConfig = { ...node.ichimokuBbPair, ...patch };
const rebuilt = next.mode === 'buy'
? buildIchimokuBbBuyTree(def, next)
: buildIchimokuBbSellTree(def, next);
return { ...rebuilt, id: node.id };
}
export function patchIchimokuBbPairInTree(
root: LogicNode | null,
pairNodeId: string,
patch: Partial<IchimokuBbPairConfig>,
def: IndicatorPeriodDef,
): LogicNode | null {
if (!root) return null;
if (root.id === pairNodeId && isIchimokuBbPairRoot(root)) {
return setIchimokuBbPairConfig(root, patch, def);
}
if (!root.children?.length) return root;
let changed = false;
const children = root.children.map(c => {
const patched = patchIchimokuBbPairInTree(c, pairNodeId, patch, def);
if (patched !== c) changed = true;
return patched ?? c;
});
return changed ? { ...root, children } : root;
}
+4 -2
View File
@@ -101,8 +101,10 @@ const PLOT_LABELS: Record<string, LabelPair> = {
Middle: { ko: '중간', en: 'Middle' },
Basis: { ko: '기준', en: 'Basis' },
: { ko: '중앙값', en: 'Basis' },
: { ko: '어퍼', en: 'Upper' },
: { ko: '로우어', en: 'Lower' },
: { ko: '상한선', en: 'Upper' },
: { ko: '하한선', en: 'Lower' },
: { ko: '상한선', en: 'Upper' },
: { ko: '하한선', en: 'Lower' },
Tenkan: { ko: '전환선', en: 'Tenkan' },
Kijun: { ko: '기준선', en: 'Kijun' },
SpanA: { ko: '선행스팬1', en: 'SpanA' },
+1 -1
View File
@@ -359,7 +359,7 @@ export const INDICATOR_REGISTRY: IndicatorDef[] = [
{ type:'MACross',name:'MA Cross', koreanName:'이동평균교차', shortName:'MACross',category:'Moving Averages', overlay:true, defaultParams:{fastLength:9, slowLength:21, src:'close'},plots:[{id:'plot0',title:'Fast', color:'#2962FF',type:'line',lineWidth:2},{id:'plot1',title:'Slow',color:'#FF6D00',type:'line',lineWidth:2}], description:'이동평균 교차 (골든크로스/데드크로스)' },
// ── Channels & Bands ──────────────────────────────────────────────────────
{ type:'BollingerBands', name:'Bollinger Bands', koreanName:'볼린저밴드', shortName:'BB', category:'Channels & Bands', overlay:true, defaultParams:{symbolMode:'main', refSymbol:'', length:20, mult:2, offset:0, src:'close', maType:'SMA'}, plots:[{id:'plot0',title:'중앙값',color:'#FF9800',type:'line',lineWidth:1},{id:'plot1',title:'어퍼',color:'#42A5F5',type:'line',lineWidth:1},{id:'plot2',title:'로우어',color:'#42A5F5',type:'line',lineWidth:1}], description:'볼린저 밴드 (SMA ± σ×곱)' },
{ type:'BollingerBands', name:'Bollinger Bands', koreanName:'볼린저밴드', shortName:'BB', category:'Channels & Bands', overlay:true, defaultParams:{symbolMode:'main', refSymbol:'', length:20, mult:2, offset:0, src:'close', maType:'SMA'}, plots:[{id:'plot0',title:'중앙값',color:'#FF9800',type:'line',lineWidth:1},{id:'plot1',title:'상한선',color:'#42A5F5',type:'line',lineWidth:1},{id:'plot2',title:'하한선',color:'#42A5F5',type:'line',lineWidth:1}], description:'볼린저 밴드 (SMA ± σ×곱)' },
{ type:'KeltnerChannels',name:'Keltner Channels', koreanName:'켈트너채널', shortName:'KC', category:'Channels & Bands', overlay:true, defaultParams:{length:20, multiplier:2}, plots:[{id:'plot0',title:'Upper', color:'#7E57C2',type:'line',lineWidth:1},{id:'plot1',title:'Middle',color:'#EF5350',type:'line',lineWidth:1},{id:'plot2',title:'Lower',color:'#7E57C2',type:'line',lineWidth:1}], description:'켈트너 채널' },
{ type:'DonchianChannels',name:'Donchian Channels', koreanName:'돈치안채널', shortName:'DC', category:'Channels & Bands', overlay:true, defaultParams:{length:20}, plots:[{id:'plot0',title:'Upper', color:'#4CAF50',type:'line',lineWidth:1},{id:'plot1',title:'Basis',color:'#FFC107',type:'line',lineWidth:1},{id:'plot2',title:'Lower',color:'#4CAF50',type:'line',lineWidth:1}], description:'돈치안 채널' },
{ type:'Envelope', name:'Envelope', koreanName:'엔벨로프', shortName:'Env', category:'Channels & Bands', overlay:true, defaultParams:{length:20, percent:0.1, src:'close'}, plots:[{id:'plot0',title:'Upper', color:'#009688',type:'line',lineWidth:1},{id:'plot1',title:'Basis',color:'#607D8B',type:'line',lineWidth:1},{id:'plot2',title:'Lower',color:'#009688',type:'line',lineWidth:1}], description:'엔벨로프 채널' },
+56 -1
View File
@@ -16,6 +16,7 @@ export type StableStrategyId =
| 'MA_TREND'
| 'ICHIMOKU_TREND'
| 'ICHIMOKU_SANYAKU'
| 'ICHIMOKU_PERFECT'
| 'MACD_MOMENTUM'
| 'BOLLINGER_RANGE';
@@ -57,6 +58,8 @@ export const ICHIMOKU_TREND_BUY = 'ICHIMOKU_TREND_BUY';
export const ICHIMOKU_TREND_SELL = 'ICHIMOKU_TREND_SELL';
export const ICHIMOKU_SANYAKU_BUY = 'ICHIMOKU_SANYAKU_BUY';
export const ICHIMOKU_SANYAKU_SELL = 'ICHIMOKU_SANYAKU_SELL';
export const ICHIMOKU_PERFECT_BUY = 'ICHIMOKU_PERFECT_BUY';
export const ICHIMOKU_PERFECT_SELL = 'ICHIMOKU_PERFECT_SELL';
export const MACD_MOMENTUM_BUY = 'MACD_MOMENTUM_BUY';
export const MACD_MOMENTUM_SELL = 'MACD_MOMENTUM_SELL';
export const BOLLINGER_RANGE_BUY = 'BOLLINGER_RANGE_BUY';
@@ -75,6 +78,8 @@ const VALUE_TO_META: Record<string, { id: StableStrategyId; mode: 'buy' | 'sell'
[ICHIMOKU_TREND_SELL]: { id: 'ICHIMOKU_TREND', mode: 'sell' },
[ICHIMOKU_SANYAKU_BUY]: { id: 'ICHIMOKU_SANYAKU', mode: 'buy' },
[ICHIMOKU_SANYAKU_SELL]: { id: 'ICHIMOKU_SANYAKU', mode: 'sell' },
[ICHIMOKU_PERFECT_BUY]: { id: 'ICHIMOKU_PERFECT', mode: 'buy' },
[ICHIMOKU_PERFECT_SELL]: { id: 'ICHIMOKU_PERFECT', mode: 'sell' },
[MACD_MOMENTUM_BUY]: { id: 'MACD_MOMENTUM', mode: 'buy' },
[MACD_MOMENTUM_SELL]: { id: 'MACD_MOMENTUM', mode: 'sell' },
[BOLLINGER_RANGE_BUY]: { id: 'BOLLINGER_RANGE', mode: 'buy' },
@@ -151,6 +156,16 @@ export const STABLE_STRATEGY_PALETTE_ITEMS: StableStrategyPaletteMeta[] = [
desc: '전환·기준·구름 하향 돌파(현재봉 1회) — 유지 조건 없음',
periodHint: '9 / 26 / 52',
},
{
value: ICHIMOKU_PERFECT_BUY, label: '일목 5원소 매수', strategyId: 'ICHIMOKU_PERFECT', mode: 'buy',
desc: '괘·주가·구름·후행 4중 AND — 전환>기준 + 구름 위 + 양운 + 후행>26봉전 종가',
periodHint: '9 / 26 / 52',
},
{
value: ICHIMOKU_PERFECT_SELL, label: '일목 5원소 매도', strategyId: 'ICHIMOKU_PERFECT', mode: 'sell',
desc: '괘·주가·구름·후행 4중 AND — 전환<기준 + 구름 아래 + 음운 + 후행<26봉전 종가',
periodHint: '9 / 26 / 52',
},
{
value: MACD_MOMENTUM_BUY, label: 'MACD 모멘텀 매수', strategyId: 'MACD_MOMENTUM', mode: 'buy',
desc: 'MACD 상향돌파 + EMA60 위 + ADX',
@@ -472,6 +487,34 @@ function buildIchimokuSanyakuSell(def: IndicatorPeriodDef, level: StableStrategy
return wrapOrRoot(branches, pairMeta('ICHIMOKU_SANYAKU', 'sell', level));
}
/**
* 일목 5원소 입체 매매 — ta4j PerfectIchimokuStrategy 와 동일한 4중 AND 필터.
* 괘(전환/기준) · 주가(구름 위/아래) · 구름(양운/음운) · 후행(26봉전 종가 대비).
*/
function buildIchimokuPerfectBuy(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode {
const nodes: LogicNode[] = [
makeCondition('ICHIMOKU', 'GT', 'CONVERSION_LINE', 'BASE_LINE', def),
makeCondition('ICHIMOKU', 'ABOVE_CLOUD', 'CLOSE_PRICE', 'NONE', def),
];
if (level !== 'relaxed') {
nodes.push(makeCondition('ICHIMOKU', 'SPAN1_GT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2', def));
nodes.push(makeCondition('ICHIMOKU', 'LAGGING_GT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE', def));
}
return wrapAnd(nodes, pairMeta('ICHIMOKU_PERFECT', 'buy', level));
}
function buildIchimokuPerfectSell(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode {
const nodes: LogicNode[] = [
makeCondition('ICHIMOKU', 'LT', 'CONVERSION_LINE', 'BASE_LINE', def),
makeCondition('ICHIMOKU', 'BELOW_CLOUD', 'CLOSE_PRICE', 'NONE', def),
];
if (level !== 'relaxed') {
nodes.push(makeCondition('ICHIMOKU', 'SPAN1_LT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2', def));
nodes.push(makeCondition('ICHIMOKU', 'LAGGING_LT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE', def));
}
return wrapAnd(nodes, pairMeta('ICHIMOKU_PERFECT', 'sell', level));
}
function buildMacdMomentumBuy(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode {
const nodes: LogicNode[] = [
makeCondition('MACD', 'CROSS_UP', 'MACD_LINE', 'SIGNAL_LINE', def),
@@ -522,6 +565,7 @@ const BUILDERS: Record<StableStrategyId, {
MA_TREND: { buy: buildMaTrendBuy, sell: buildMaTrendSell },
ICHIMOKU_TREND: { buy: buildIchimokuTrendBuy, sell: buildIchimokuTrendSell },
ICHIMOKU_SANYAKU: { buy: buildIchimokuSanyakuBuy, sell: buildIchimokuSanyakuSell },
ICHIMOKU_PERFECT: { buy: buildIchimokuPerfectBuy, sell: buildIchimokuPerfectSell },
MACD_MOMENTUM: { buy: buildMacdMomentumBuy, sell: buildMacdMomentumSell },
BOLLINGER_RANGE: { buy: buildBollingerRangeBuy, sell: buildBollingerRangeSell },
};
@@ -559,7 +603,8 @@ export function stableStrategySummaryText(
CROSS_UP: '상향돌파', CROSS_DOWN: '하향돌파',
GT: '>', LT: '<', GTE: '≥', LTE: '≤',
ABOVE_CLOUD: '구름 위', BELOW_CLOUD: '구름 아래',
SPAN1_GT_SPAN2: '양운(선행1>2)', LAGGING_GT_PRICE: '후행>26봉전 종가',
SPAN1_GT_SPAN2: '양운(선행1>2)', SPAN1_LT_SPAN2: '음운(선행1<2)',
LAGGING_GT_PRICE: '후행>26봉전 종가', LAGGING_LT_PRICE: '후행<26봉전 종가',
LAGGING_ABOVE_PRIOR_CLOUD: '후행>26봉전 구름', VOLUME_GT_MA_RATIO: '거래량≥MA×비율',
};
const fmt = (n: LogicNode): string => {
@@ -595,6 +640,10 @@ export function stableStrategyFilterSummary(
else if (level === 'balanced') parts.push(`돌파1회·필터N${w}·(전환>기준 OR 양운)`);
else parts.push('돌파1회·종가>전환');
}
if (strategyId === 'ICHIMOKU_PERFECT') {
if (level === 'relaxed') parts.push('괘·주가(구름) 2조건만');
else parts.push('괘·주가·구름·후행 4중 AND');
}
if (strategyId === 'MACD_MOMENTUM' && level === 'relaxed') parts.push('EMA60·ADX 없음');
if (strategyId === 'MA_TREND' && level === 'relaxed') parts.push('ADX 없음');
return parts.join(', ');
@@ -639,6 +688,12 @@ export function inferStableStrategyFilterLevel(node: LogicNode): StableStrategyF
if (hasLagging && (hasCloudBreak || hasChikouCross)) return 'balanced';
if (hasCloudBreak || hasChikouCross) return 'relaxed';
}
if (id === 'ICHIMOKU_PERFECT') {
const hasSpan = conds.some(c => c.conditionType === 'SPAN1_GT_SPAN2' || c.conditionType === 'SPAN1_LT_SPAN2');
const hasLagging = conds.some(c => c.conditionType === 'LAGGING_GT_PRICE' || c.conditionType === 'LAGGING_LT_PRICE');
if (!hasSpan && !hasLagging) return 'relaxed';
return 'balanced';
}
if (id === 'MACD_MOMENTUM' && !conds.some(c => c.rightField === emaField(60))) return 'relaxed';
if (id === 'MA_TREND' && !conds.some(c => c.indicatorType === 'ADX')) return 'relaxed';
if (id === 'BOLLINGER_RANGE') {
+32 -6
View File
@@ -44,6 +44,14 @@ import {
inferInflection33FilterLevel,
inflection33FilterLevelLabel,
} from '../utils/inflection33Strategy';
import {
buildIchimokuBbTree,
isIchimokuBbPairRoot,
isIchimokuBbPaletteValue,
ichimokuBbDisplayName,
ichimokuBbSummaryText,
inferIchimokuBbConfig,
} from '../utils/ichimokuBbStrategy';
import {
buildStableStrategyTree,
isStableStrategyPairRoot,
@@ -704,18 +712,18 @@ function buildFieldOptsForSlot(
return slot === 'right'
? [
NONE,
{ value: 'UPPER_BAND', label: `단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'MIDDLE_BAND', label: `심선(${DEF.bbPeriod}일)` },
{ value: 'LOWER_BAND', label: `단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'UPPER_BAND', label: `한선(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'MIDDLE_BAND', label: `앙값(${DEF.bbPeriod}일)` },
{ value: 'LOWER_BAND', label: `한선(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'CLOSE_PRICE', label: '종가' },
{ value: 'OPEN_PRICE', label: '시가' },
{ value: 'HIGH_PRICE', label: '고가' },
{ value: 'LOW_PRICE', label: '저가' },
]
: [
{ value: 'UPPER_BAND', label: `단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'MIDDLE_BAND', label: `심선(${DEF.bbPeriod}일)` },
{ value: 'LOWER_BAND', label: `단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'UPPER_BAND', label: `한선(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'MIDDLE_BAND', label: `앙값(${DEF.bbPeriod}일)` },
{ value: 'LOWER_BAND', label: `한선(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
{ value: 'CLOSE_PRICE', label: '종가' },
{ value: 'OPEN_PRICE', label: '시가' },
{ value: 'HIGH_PRICE', label: '고가' },
@@ -985,6 +993,10 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
const level = inferInflection33FilterLevel(node);
return `${inflection33DisplayName(mode, period)} [필터:${inflection33FilterLevelLabel(level)}]\n${inflection33SummaryText(mode, period, level)}`;
}
if (isIchimokuBbPairRoot(node) && node.ichimokuBbPair?.mode === 'buy') {
const cfg = inferIchimokuBbConfig(node);
return `${ichimokuBbDisplayName(cfg.mode, cfg.chikouDisplacement)}\n${ichimokuBbSummaryText(cfg)}`;
}
if (isPriceExtremeBreakoutPairRoot(node) && node.priceExtremePair) {
const { mode, shortPeriod, longPeriod } = node.priceExtremePair;
const level = inferPriceExtremeFilterLevel(node);
@@ -1004,6 +1016,10 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
const level = inferInflection33FilterLevel(node);
return `${inflection33DisplayName(mode, period)} [필터:${inflection33FilterLevelLabel(level)}]\n${inflection33SummaryText(mode, period, level)}`;
}
if (isIchimokuBbPairRoot(node) && node.ichimokuBbPair?.mode === 'sell') {
const cfg = inferIchimokuBbConfig(node);
return `${ichimokuBbDisplayName(cfg.mode, cfg.chikouDisplacement)}\n${ichimokuBbSummaryText(cfg)}`;
}
if (isStochOverboughtPairRoot(node)) {
const sec = node.stochPair!.secondaryIndicatorType;
return `${stochPairDisplayName(sec)}\n${stochPairSummaryText(sec)}`;
@@ -1527,6 +1543,8 @@ export type MakeNodeOptions = {
priceExtremeBreakout?: boolean;
/** 33변곡 EMA+채널 복합 */
inflection33?: boolean;
/** 일목 후행×볼린저 복합 */
ichimokuBb?: boolean;
/** 추천 안정형 전략 복합 */
stableStrategy?: boolean;
};
@@ -1537,6 +1555,7 @@ export function makeNodeOptionsFromPalette(data: {
stochPair?: boolean;
priceExtremeBreakout?: boolean;
inflection33?: boolean;
ichimokuBb?: boolean;
stableStrategy?: boolean;
secondaryIndicator?: string;
period?: number;
@@ -1558,6 +1577,9 @@ export function makeNodeOptionsFromPalette(data: {
if (data.inflection33 || (data.value && isInflection33PaletteValue(data.value))) {
return { inflection33: true };
}
if (data.ichimokuBb || (data.value && isIchimokuBbPaletteValue(data.value))) {
return { ichimokuBb: true };
}
if (data.stableStrategy || (data.value && isStableStrategyPaletteValue(data.value))) {
return { stableStrategy: true };
}
@@ -1590,6 +1612,10 @@ export const makeNode = (
const tree = buildInflection33Tree(value, DEF);
if (tree) return tree;
}
if (options?.ichimokuBb || isIchimokuBbPaletteValue(value)) {
const tree = buildIchimokuBbTree(value, DEF);
if (tree) return tree;
}
if (options?.stableStrategy || isStableStrategyPaletteValue(value)) {
const tree = buildStableStrategyTree(value, DEF);
if (tree) return tree;
+13 -2
View File
@@ -48,6 +48,14 @@ export type StrategyFlowNodeData = {
onUpdatePriceExtremeFilterLevel?: (nodeId: string, filterLevel: 'strict' | 'balanced' | 'relaxed') => void;
/** 33변곡 EMA+채널 복합 — 필터 강도 변경 */
onUpdateInflection33FilterLevel?: (nodeId: string, filterLevel: 'strict' | 'balanced' | 'relaxed') => void;
onUpdateIchimokuBbConfig?: (
nodeId: string,
patch: Partial<{
chikouDisplacement: number;
bbBand: 'UPPER_BAND' | 'MIDDLE_BAND' | 'LOWER_BAND';
conditionType: 'CROSS_UP' | 'CROSS_DOWN' | 'GT' | 'LT';
}>,
) => void;
/** 추천 안정형 전략 — 필터 강도 변경 */
onUpdateStableStrategyFilterLevel?: (nodeId: string, filterLevel: 'strict' | 'balanced' | 'relaxed') => void;
/** AND ↔ OR 전환 */
@@ -482,7 +490,7 @@ function layoutTree(
signalTab: 'buy' | 'sell',
callbacks: Pick<
StrategyFlowNodeData,
'onDelete' | 'onUpdateCondition' | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType'
'onDelete' | 'onUpdateCondition' | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateIchimokuBbConfig' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType'
| 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget'
>,
dragOverId: string | null,
@@ -508,6 +516,7 @@ function layoutTree(
onUpdateStochPairSecondary: callbacks.onUpdateStochPairSecondary,
onUpdatePriceExtremeFilterLevel: callbacks.onUpdatePriceExtremeFilterLevel,
onUpdateInflection33FilterLevel: callbacks.onUpdateInflection33FilterLevel,
onUpdateIchimokuBbConfig: callbacks.onUpdateIchimokuBbConfig,
onUpdateStableStrategyFilterLevel: callbacks.onUpdateStableStrategyFilterLevel,
onChangeLogicGateType: callbacks.onChangeLogicGateType,
onDropTarget: callbacks.onDropTarget,
@@ -638,7 +647,7 @@ function appendOrphanFlowNodes(
signalTab: 'buy' | 'sell',
callbacks: Pick<
StrategyFlowNodeData,
'onDelete' | 'onUpdateCondition' | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType'
'onDelete' | 'onUpdateCondition' | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateIchimokuBbConfig' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType'
| 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget'
>,
): void {
@@ -664,6 +673,7 @@ function appendOrphanFlowNodes(
onUpdateStochPairSecondary: callbacks.onUpdateStochPairSecondary,
onUpdatePriceExtremeFilterLevel: callbacks.onUpdatePriceExtremeFilterLevel,
onUpdateInflection33FilterLevel: callbacks.onUpdateInflection33FilterLevel,
onUpdateIchimokuBbConfig: callbacks.onUpdateIchimokuBbConfig,
onUpdateStableStrategyFilterLevel: callbacks.onUpdateStableStrategyFilterLevel,
onChangeLogicGateType: callbacks.onChangeLogicGateType,
onDropTarget: callbacks.onDropTarget,
@@ -692,6 +702,7 @@ export function logicNodeToFlow(
| 'onUpdateStochPairSecondary'
| 'onUpdatePriceExtremeFilterLevel'
| 'onUpdateInflection33FilterLevel'
| 'onUpdateIchimokuBbConfig'
| 'onUpdateStableStrategyFilterLevel'
| 'onDropTarget'
| 'onDragOverTarget'
@@ -10,6 +10,11 @@ import {
INFLECTION_33_SELL_VALUE,
inflection33PaletteDesc,
} from './inflection33Strategy';
import {
ICHIMOKU_BB_BUY_VALUE,
ICHIMOKU_BB_SELL_VALUE,
ichimokuBbPaletteDesc,
} from './ichimokuBbStrategy';
import {
STABLE_STRATEGY_PALETTE_ITEMS,
} from './stableStrategyPairs';
@@ -97,6 +102,20 @@ export const DEFAULT_COMPOSITE_ITEMS: Omit<PaletteItem, 'id' | 'kind'>[] = [
builtIn: true,
period: 33,
},
{
value: ICHIMOKU_BB_BUY_VALUE,
label: '일목×BB 매수',
desc: ichimokuBbPaletteDesc(ICHIMOKU_BB_BUY_VALUE),
builtIn: true,
period: 26,
},
{
value: ICHIMOKU_BB_SELL_VALUE,
label: '일목×BB 매도',
desc: ichimokuBbPaletteDesc(ICHIMOKU_BB_SELL_VALUE),
builtIn: true,
period: 26,
},
...STABLE_STRATEGY_PALETTE_ITEMS.map(i => ({
value: i.value,
label: i.label,
+20 -4
View File
@@ -73,6 +73,13 @@ export interface LogicNode {
mode: 'buy' | 'sell';
filterLevel?: 'strict' | 'balanced' | 'relaxed';
};
/** 일목 후행스팬 × 볼린저밴드 복합 */
ichimokuBbPair?: {
mode: 'buy' | 'sell';
chikouDisplacement: number;
bbBand: 'UPPER_BAND' | 'MIDDLE_BAND' | 'LOWER_BAND';
conditionType: 'CROSS_UP' | 'CROSS_DOWN' | 'GT' | 'LT';
};
}
export interface StrategyDSLDto {
@@ -130,6 +137,7 @@ export const INDICATOR_CONDITIONS: Record<string, string[]> = {
'LAGGING_GT_PRICE', 'LAGGING_LT_PRICE', 'LAGGING_ABOVE_PRIOR_CLOUD',
'CHIKOU_CROSS_ABOVE_PRIOR_CLOUD',
],
ICHIMOKU_BB: ['GT', 'LT', 'GTE', 'LTE', 'CROSS_UP', 'CROSS_DOWN'],
};
// ── 지표별 leftField 옵션 ───────────────────────────────────────────────────
@@ -167,8 +175,8 @@ export const LEFT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
],
BOLLINGER: [
{ value: 'CLOSE_PRICE', label: '종가' },
{ value: 'UPPER', label: '상단밴드' }, { value: 'MIDDLE', label: '중단밴드' },
{ value: 'LOWER', label: '하단밴드' },
{ value: 'UPPER', label: '상한선' }, { value: 'MIDDLE', label: '중앙값' },
{ value: 'LOWER', label: '하한선' },
],
DONCHIAN: [
{ value: 'CLOSE_PRICE', label: '종가' },
@@ -189,6 +197,9 @@ export const LEFT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
{ value: 'LEADING_SPAN1', label: '선행1' }, { value: 'LEADING_SPAN2', label: '선행2' },
{ value: 'LAGGING_SPAN', label: '후행스팬' },
],
ICHIMOKU_BB: [
{ value: 'LAGGING_SPAN', label: '후행스팬' },
],
};
export const RIGHT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
@@ -250,8 +261,8 @@ export const RIGHT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
{ value: 'CUSTOM', label: '직접 입력' },
],
BOLLINGER: [
{ value: 'UPPER', label: '상단밴드' }, { value: 'MIDDLE', label: '중단밴드' },
{ value: 'LOWER', label: '하단밴드' }, { value: 'CUSTOM', label: '직접 입력' },
{ value: 'UPPER', label: '상한선' }, { value: 'MIDDLE', label: '중앙값' },
{ value: 'LOWER', label: '하한선' }, { value: 'CUSTOM', label: '직접 입력' },
],
DONCHIAN: [
{ value: 'DC_UPPER_9', label: '상단(9일 최고가)' },
@@ -282,6 +293,11 @@ export const RIGHT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
{ value: 'LEADING_SPAN1', label: '선행1' }, { value: 'LEADING_SPAN2', label: '선행2' },
{ value: 'LAGGING_SPAN', label: '후행스팬' }, { value: 'CLOSE_PRICE', label: '종가' },
],
ICHIMOKU_BB: [
{ value: 'UPPER_BAND', label: '상한선' },
{ value: 'MIDDLE_BAND', label: '중앙값' },
{ value: 'LOWER_BAND', label: '하한선' },
],
VOLUME: [{ value: 'MA5', label: 'MA(5일)' }, { value: 'MA20', label: 'MA(20일)' }, { value: 'CUSTOM', label: '직접 입력' }],
DISPARITY: [{ value: 'NEUTRAL_100', label: '기준(100)' }, { value: 'OVERBOUGHT_105', label: '과매수(105)' }, { value: 'OVERSOLD_95', label: '과매도(95)' }, { value: 'CUSTOM', label: '직접 입력' }],
};