일목균형표 복합지표 추가

This commit is contained in:
Macbook
2026-06-25 01:00:23 +09:00
parent 80cf8daf55
commit 4ab8053b65
15 changed files with 3588 additions and 6 deletions
@@ -91,6 +91,10 @@ import {
type StableStrategyFilterLevel,
type StableStrategyId,
} from '../utils/stableStrategyPairs';
import {
buildIchimokuSituationTree,
isIchimokuSituationPaletteValue,
} from '../utils/ichimokuSituationStrategy';
import {
buildStochOverboughtPairTree,
findStochPairRootInForest,
@@ -1367,7 +1371,8 @@ export default function StrategyEditorPage({
const inflection33 = isInflection33PaletteValue(item.value);
const ichimokuBb = isIchimokuBbPaletteValue(item.value);
const stableStrategy = isStableStrategyPaletteValue(item.value);
const composite = item.kind === 'composite' && !stochPair && !priceExtremeBreakout && !inflection33 && !ichimokuBb && !stableStrategy;
const ichimokuSituation = isIchimokuSituationPaletteValue(item.value);
const composite = item.kind === 'composite' && !stochPair && !priceExtremeBreakout && !inflection33 && !ichimokuBb && !stableStrategy && !ichimokuSituation;
const newNode = stochPair
? buildStochOverboughtPairTree(item.secondaryIndicator ?? 'CCI', DEF)
: priceExtremeBreakout
@@ -1378,6 +1383,8 @@ export default function StrategyEditorPage({
? buildIchimokuBbTree(item.value, DEF)
: stableStrategy
? buildStableStrategyTree(item.value, DEF)
: ichimokuSituation
? buildIchimokuSituationTree(item.value, DEF)
: makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined);
if (!newNode) return;
handleEditorStateChange(prev => {
@@ -31,6 +31,11 @@ import {
inferStableStrategyFilterLevel,
type StableStrategyId,
} from '../../utils/stableStrategyPairs';
import {
isIchimokuSituationPairRoot,
ichimokuSituationDisplayName,
type IchimokuSituationId,
} from '../../utils/ichimokuSituationStrategy';
import StableStrategyPairNodeSettings from './StableStrategyPairNodeSettings';
import {
isStochOverboughtPairRoot,
@@ -241,12 +246,13 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
const isInflection33Pair = isInflection33PairRoot(node);
const isIchimokuBbPair = isIchimokuBbPairRoot(node);
const isStableStrategyPair = isStableStrategyPairRoot(node);
const isIchimokuSituationPair = isIchimokuSituationPairRoot(node);
const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
const [settingsOpen, setSettingsOpen] = useState(false);
return (
<div
className={`se-flow-node se-flow-node--logic${isStochPair ? ' se-flow-node--stoch-pair' : ''}${isPriceExtremePair ? ' se-flow-node--price-extreme-pair' : ''}${isInflection33Pair ? ' se-flow-node--inflection33-pair' : ''}${isIchimokuBbPair ? ' se-flow-node--ichimoku-bb-pair' : ''}${isStableStrategyPair ? ' se-flow-node--stable-strategy-pair' : ''}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? ' se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`}
className={`se-flow-node se-flow-node--logic${isStochPair ? ' se-flow-node--stoch-pair' : ''}${isPriceExtremePair ? ' se-flow-node--price-extreme-pair' : ''}${isInflection33Pair ? ' se-flow-node--inflection33-pair' : ''}${isIchimokuBbPair ? ' se-flow-node--ichimoku-bb-pair' : ''}${isStableStrategyPair ? ' se-flow-node--stable-strategy-pair' : ''}${isIchimokuSituationPair ? ' se-flow-node--ichimoku-situation-pair' : ''}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? ' se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`}
style={{ '--se-gate-color': color } as React.CSSProperties}
onDragEnter={onDragEnter}
onDragOver={onDragOver}
@@ -297,6 +303,16 @@ export const LogicGateNode = memo(function LogicGateNode({ id, data, selected }:
</button>
</div>
)}
{isIchimokuSituationPair && node.ichimokuSituationPair && (
<div className="se-flow-gate-stoch-pair">
<span className="se-flow-gate-stoch-pair-title">
{ichimokuSituationDisplayName(
node.ichimokuSituationPair.situationId as IchimokuSituationId,
node.ichimokuSituationPair.mode,
)}
</span>
</div>
)}
{isInflection33Pair && node.inflection33Pair && (
<div className="se-flow-gate-stoch-pair">
<span className="se-flow-gate-stoch-pair-title">
@@ -8,6 +8,7 @@ import { isPriceExtremeBreakoutPairPaletteValue } from '../../utils/priceExtreme
import { isInflection33PaletteValue } from '../../utils/inflection33Strategy';
import { isIchimokuBbPaletteValue } from '../../utils/ichimokuBbStrategy';
import { isStableStrategyPaletteValue, STABLE_STRATEGY_PALETTE_ITEMS } from '../../utils/stableStrategyPairs';
import { isIchimokuSituationPaletteValue } from '../../utils/ichimokuSituationStrategy';
import { getDefaultIndicatorPeriod } from '../../utils/conditionPeriods';
import { getStrategyIndicatorDisplayName } from '../../utils/strategyPaletteStorage';
import { getIndicatorPeriodLabel } from '../../utils/strategyFlowLayout';
@@ -39,6 +40,9 @@ function periodLabel(item: PaletteItem, def: DefType): string {
const meta = STABLE_STRATEGY_PALETTE_ITEMS.find(i => i.value === item.value);
return meta?.periodHint ?? '';
}
if (item.kind === 'composite' && isIchimokuSituationPaletteValue(item.value)) {
return '9 / 26 / 52';
}
if (item.kind === 'composite') {
const d = getCompositeDefaultPeriods(item.value, def);
return `${d.short} / ${d.long}`;
@@ -194,12 +198,14 @@ export default function IndicatorPaletteTab({
&& !isPriceExtremeBreakoutPairPaletteValue(item.value)
&& !isInflection33PaletteValue(item.value)
&& !isIchimokuBbPaletteValue(item.value)
&& !isStableStrategyPaletteValue(item.value)}
&& !isStableStrategyPaletteValue(item.value)
&& !isIchimokuSituationPaletteValue(item.value)}
stochPair={isStochOverboughtPairPaletteValue(item.value)}
priceExtremeBreakout={isPriceExtremeBreakoutPairPaletteValue(item.value)}
inflection33={isInflection33PaletteValue(item.value)}
ichimokuBb={isIchimokuBbPaletteValue(item.value)}
stableStrategy={isStableStrategyPaletteValue(item.value)}
ichimokuSituation={isIchimokuSituationPaletteValue(item.value)}
secondaryIndicator={item.secondaryIndicator}
period={periodLabel(item, def)}
periodValue={item.period}
@@ -29,6 +29,7 @@ interface Props {
inflection33?: boolean;
ichimokuBb?: boolean;
stableStrategy?: boolean;
ichimokuSituation?: boolean;
secondaryIndicator?: string;
selected?: boolean;
onSelect?: () => void;
@@ -37,7 +38,7 @@ interface Props {
export default function PaletteChip({
type, value, label, desc, color, period, periodValue, shortPeriod, longPeriod,
composite = false, stochPair = false, priceExtremeBreakout = false, inflection33 = false, ichimokuBb = false, stableStrategy = false, secondaryIndicator, selected = false, onSelect, onAdd,
composite = false, stochPair = false, priceExtremeBreakout = false, inflection33 = false, ichimokuBb = false, stableStrategy = false, ichimokuSituation = false, secondaryIndicator, selected = false, onSelect, onAdd,
}: Props) {
const buildPayload = (): PaletteDragPayload => ({
type, value, label,
@@ -47,6 +48,7 @@ export default function PaletteChip({
inflection33: inflection33 || undefined,
ichimokuBb: ichimokuBb || undefined,
stableStrategy: stableStrategy || undefined,
ichimokuSituation: ichimokuSituation || undefined,
secondaryIndicator,
period: periodValue,
shortPeriod,
@@ -794,6 +794,9 @@ function StrategyEditorCanvasInner({
: n.stableStrategyPair?.mode === 'sell'
? (gateType === 'OR' ? n.stableStrategyPair : undefined)
: undefined,
ichimokuSituationPair: n.ichimokuSituationPair
? n.ichimokuSituationPair
: undefined,
}
: n
);
@@ -51,6 +51,10 @@ import {
buildStableStrategyTree,
isStableStrategyPaletteValue,
} from '../utils/stableStrategyPairs';
import {
buildIchimokuSituationTree,
isIchimokuSituationPaletteValue,
} from '../utils/ichimokuSituationStrategy';
import {
buildSidewaysFilterNode,
loadSidewaysPaletteItems,
@@ -453,7 +457,8 @@ export function useStrategyEvaluationEditor({
const inflection33 = isInflection33PaletteValue(item.value);
const ichimokuBb = isIchimokuBbPaletteValue(item.value);
const stableStrategy = isStableStrategyPaletteValue(item.value);
const composite = item.kind === 'composite' && !stochPair && !priceExtremeBreakout && !inflection33 && !ichimokuBb && !stableStrategy;
const ichimokuSituation = isIchimokuSituationPaletteValue(item.value);
const composite = item.kind === 'composite' && !stochPair && !priceExtremeBreakout && !inflection33 && !ichimokuBb && !stableStrategy && !ichimokuSituation;
const newNode = stochPair
? buildStochOverboughtPairTree(item.secondaryIndicator ?? 'CCI', def)
: priceExtremeBreakout
@@ -464,6 +469,8 @@ export function useStrategyEvaluationEditor({
? buildIchimokuBbTree(item.value, def)
: stableStrategy
? buildStableStrategyTree(item.value, def)
: ichimokuSituation
? buildIchimokuSituationTree(item.value, def)
: makeNode('indicator', item.value, signalTab, def, composite ? { composite: true } : undefined);
if (!newNode) return;
handleEditorStateChange(prev => {
@@ -0,0 +1,358 @@
/**
* 일목균형표 상황별·엘리어트 파동 복합 전략
* @see 일목균형표 전략.md
*/
import type { ConditionDSL, LogicNode } from './strategyTypes';
import { initConditionPeriodsInherit, type IndicatorPeriodDef } from './conditionPeriods';
import { genId } from './strategyNodeIds';
export type IchimokuSituationCategory = 'basic' | 'alignment' | 'elliott';
export type IchimokuSituationId =
| 'GOLDEN_CROSS'
| 'BASE_BREAKUP'
| 'CLOUD_BREAKUP'
| 'LAGGING_BULL'
| 'CLOUD_SUPPORT'
| 'DEAD_CROSS'
| 'BASE_BREAKDOWN'
| 'CLOUD_BREAKDOWN'
| 'LAGGING_BEAR'
| 'CLOUD_RESISTANCE'
| 'SANYAKU_REVERSE'
| 'HOJEON'
| 'GYEOKJEON'
| 'EW1'
| 'EW2'
| 'EW3'
| 'EW4'
| 'EW5'
| 'EW_A'
| 'EW_B'
| 'EW_C';
const SITUATION_PREFIX = 'ICHIMOKU_SIT_';
export function ichimokuSituationPaletteValue(id: IchimokuSituationId, mode: 'buy' | 'sell'): string {
return `${SITUATION_PREFIX}${id}_${mode.toUpperCase()}`;
}
export interface IchimokuSituationPaletteMeta {
value: string;
label: string;
desc: string;
situationId: IchimokuSituationId;
mode: 'buy' | 'sell';
category: IchimokuSituationCategory;
}
export const ICHIMOKU_SITUATION_PALETTE_ITEMS: IchimokuSituationPaletteMeta[] = [
// §2.1 기본 매수
{ value: ichimokuSituationPaletteValue('GOLDEN_CROSS', 'buy'), label: '일목 골든크로스 매수', situationId: 'GOLDEN_CROSS', mode: 'buy', category: 'basic',
desc: 'L1 — 전환선이 기준선 상향 돌파 (호전 1차)' },
{ value: ichimokuSituationPaletteValue('BASE_BREAKUP', 'buy'), label: '일목 기준선 돌파 매수', situationId: 'BASE_BREAKUP', mode: 'buy', category: 'basic',
desc: 'L2 — 종가가 기준선 상향 돌파 (중기 추세 전환)' },
{ value: ichimokuSituationPaletteValue('CLOUD_BREAKUP', 'buy'), label: '일목 구름 돌파 매수', situationId: 'CLOUD_BREAKUP', mode: 'buy', category: 'basic',
desc: 'L3 — 구름대 상향 돌파 (매물·심리 저항 돌파)' },
{ value: ichimokuSituationPaletteValue('LAGGING_BULL', 'buy'), label: '일목 후행 호전 매수', situationId: 'LAGGING_BULL', mode: 'buy', category: 'basic',
desc: 'L4 — 후행스팬 > 26봉 전 종가 (추세 확정)' },
{ value: ichimokuSituationPaletteValue('CLOUD_SUPPORT', 'buy'), label: '일목 구름 지지 매수', situationId: 'CLOUD_SUPPORT', mode: 'buy', category: 'basic',
desc: 'L5 — 종가 > 선행1·기준선 (양운·기준선 지지 반등)' },
// §1.3 호전
{ value: ichimokuSituationPaletteValue('HOJEON', 'buy'), label: '일목 호전(정배열) 매수', situationId: 'HOJEON', mode: 'buy', category: 'alignment',
desc: '괘·구름 위·양운·후행 — 완벽한 상승 추세 4조건' },
// §2.2 기본 매도
{ value: ichimokuSituationPaletteValue('DEAD_CROSS', 'sell'), label: '일목 데드크로스 매도', situationId: 'DEAD_CROSS', mode: 'sell', category: 'basic',
desc: 'S1 — 전환선이 기준선 하향 이탈' },
{ value: ichimokuSituationPaletteValue('BASE_BREAKDOWN', 'sell'), label: '일목 기준선 이탈 매도', situationId: 'BASE_BREAKDOWN', mode: 'sell', category: 'basic',
desc: 'S2 — 종가가 기준선 하향 이탈' },
{ value: ichimokuSituationPaletteValue('CLOUD_BREAKDOWN', 'sell'), label: '일목 구름 이탈 매도', situationId: 'CLOUD_BREAKDOWN', mode: 'sell', category: 'basic',
desc: 'S3 — 구름대 하향 이탈 (지지 붕괴)' },
{ value: ichimokuSituationPaletteValue('LAGGING_BEAR', 'sell'), label: '일목 후행 역전 매도', situationId: 'LAGGING_BEAR', mode: 'sell', category: 'basic',
desc: 'S4 — 후행스팬 < 26봉 전 종가' },
{ value: ichimokuSituationPaletteValue('CLOUD_RESISTANCE', 'sell'), label: '일목 구름 저항 매도', situationId: 'CLOUD_RESISTANCE', mode: 'sell', category: 'basic',
desc: 'S5 — 종가 < 선행1·기준선 (음운·기준선 저항)' },
{ value: ichimokuSituationPaletteValue('SANYAKU_REVERSE', 'sell'), label: '일목 삼역역전 매도', situationId: 'SANYAKU_REVERSE', mode: 'sell', category: 'alignment',
desc: 'S6 — 데드 + 구름 아래 + 후행 역전 (강력 청산)' },
{ value: ichimokuSituationPaletteValue('GYEOKJEON', 'sell'), label: '일목 역전(역배열) 매도', situationId: 'GYEOKJEON', mode: 'sell', category: 'alignment',
desc: '괘·구름 아래·음운·후행 — 완벽한 하락 추세 4조건' },
// §4 엘리어트 파동
{ value: ichimokuSituationPaletteValue('EW1', 'buy'), label: '일목 1파 매수', situationId: 'EW1', mode: 'buy', category: 'elliott',
desc: '추세 전환 시동 — 기준선 돌파 + 전환>기준 (소액·분할)' },
{ value: ichimokuSituationPaletteValue('EW2', 'buy'), label: '일목 2파 매수', situationId: 'EW2', mode: 'buy', category: 'elliott',
desc: '눌림목 — 기준선·구름하단(선행2) 지지 + 전환>기준' },
{ value: ichimokuSituationPaletteValue('EW2', 'sell'), label: '일목 2파 손절', situationId: 'EW2', mode: 'sell', category: 'elliott',
desc: '2파 지지 이탈 — 기준선 이탈 OR 구름 하향 돌파' },
{ value: ichimokuSituationPaletteValue('EW3', 'buy'), label: '일목 3파 매수', situationId: 'EW3', mode: 'buy', category: 'elliott',
desc: '본격 상승 — 삼역호전(괘·구름·후행·양운) 불타기' },
{ value: ichimokuSituationPaletteValue('EW4', 'buy'), label: '일목 4파 재매수', situationId: 'EW4', mode: 'buy', category: 'elliott',
desc: '횡보 조정 — 구름 상단(선행1)·기준선 지지 반등' },
{ value: ichimokuSituationPaletteValue('EW4', 'sell'), label: '일목 4파 익절', situationId: 'EW4', mode: 'sell', category: 'elliott',
desc: '4파 조정 — 기준선 이탈 OR 구름 하향 이탈' },
{ value: ichimokuSituationPaletteValue('EW5', 'sell'), label: '일목 5파 분할매도', situationId: 'EW5', mode: 'sell', category: 'elliott',
desc: '과열 — 전환선·데드·구름 이탈 (분할 청산)' },
{ value: ichimokuSituationPaletteValue('EW_A', 'sell'), label: '일목 A파 매도', situationId: 'EW_A', mode: 'sell', category: 'elliott',
desc: '하락 개시 — 기준선·데드크로스' },
{ value: ichimokuSituationPaletteValue('EW_B', 'sell'), label: '일목 B파 매도', situationId: 'EW_B', mode: 'sell', category: 'elliott',
desc: '기술적 반등 실패 — 기준선·음운 저항 (전량 대피)' },
{ value: ichimokuSituationPaletteValue('EW_C', 'sell'), label: '일목 C파 매도', situationId: 'EW_C', mode: 'sell', category: 'elliott',
desc: '본 하락 — 삼역역전 + 구름 이탈 (물타기 금지)' },
];
const VALUE_TO_META: Record<string, { situationId: IchimokuSituationId; mode: 'buy' | 'sell' }> =
Object.fromEntries(ICHIMOKU_SITUATION_PALETTE_ITEMS.map(i => [i.value, { situationId: i.situationId, mode: i.mode }]));
export function isIchimokuSituationPaletteValue(value: string): boolean {
return value.startsWith(SITUATION_PREFIX) && value in VALUE_TO_META;
}
export function isIchimokuSituationPairRoot(node: LogicNode): boolean {
return !!node.ichimokuSituationPair?.situationId
&& (node.type === 'AND' || node.type === 'OR');
}
function makeIch(
def: IndicatorPeriodDef,
conditionType: string,
leftField: string,
rightField: string,
extra?: Partial<ConditionDSL>,
): LogicNode {
const base: ConditionDSL = {
indicatorType: 'ICHIMOKU',
conditionType,
leftField,
rightField,
candleRange: 1,
valuePeriodOverride: false,
thresholdOverride: false,
rightPeriodOverride: false,
...extra,
};
return {
id: genId(),
type: 'CONDITION',
condition: initConditionPeriodsInherit('ICHIMOKU', base, def),
};
}
function pairMeta(
situationId: IchimokuSituationId,
mode: 'buy' | 'sell',
): LogicNode['ichimokuSituationPair'] {
return { situationId, mode };
}
function wrapAnd(children: LogicNode[], meta: LogicNode['ichimokuSituationPair']): LogicNode {
return { id: genId(), type: 'AND', ichimokuSituationPair: meta, children };
}
function wrapOr(children: LogicNode[], meta: LogicNode['ichimokuSituationPair']): LogicNode {
return { id: genId(), type: 'OR', ichimokuSituationPair: meta, children };
}
type Builder = (def: IndicatorPeriodDef) => LogicNode;
const BUILDERS: Record<IchimokuSituationId, Partial<Record<'buy' | 'sell', Builder>>> = {
GOLDEN_CROSS: {
buy: def => wrapAnd([
makeIch(def, 'CROSS_UP', 'CONVERSION_LINE', 'BASE_LINE'),
], pairMeta('GOLDEN_CROSS', 'buy')),
},
BASE_BREAKUP: {
buy: def => wrapAnd([
makeIch(def, 'CROSS_UP', 'CLOSE_PRICE', 'BASE_LINE'),
], pairMeta('BASE_BREAKUP', 'buy')),
},
CLOUD_BREAKUP: {
buy: def => wrapAnd([
makeIch(def, 'CLOUD_BREAK_UP', 'CLOSE_PRICE', 'NONE'),
], pairMeta('CLOUD_BREAKUP', 'buy')),
},
LAGGING_BULL: {
buy: def => wrapAnd([
makeIch(def, 'LAGGING_GT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE'),
], pairMeta('LAGGING_BULL', 'buy')),
},
CLOUD_SUPPORT: {
buy: def => wrapAnd([
makeIch(def, 'GT', 'CLOSE_PRICE', 'LEADING_SPAN1'),
makeIch(def, 'GT', 'CLOSE_PRICE', 'BASE_LINE'),
], pairMeta('CLOUD_SUPPORT', 'buy')),
},
HOJEON: {
buy: def => wrapAnd([
makeIch(def, 'GT', 'CONVERSION_LINE', 'BASE_LINE'),
makeIch(def, 'ABOVE_CLOUD', 'CLOSE_PRICE', 'NONE'),
makeIch(def, 'SPAN1_GT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2'),
makeIch(def, 'LAGGING_GT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE'),
], pairMeta('HOJEON', 'buy')),
},
DEAD_CROSS: {
sell: def => wrapOr([
makeIch(def, 'CROSS_DOWN', 'CONVERSION_LINE', 'BASE_LINE'),
], pairMeta('DEAD_CROSS', 'sell')),
},
BASE_BREAKDOWN: {
sell: def => wrapOr([
makeIch(def, 'CROSS_DOWN', 'CLOSE_PRICE', 'BASE_LINE'),
], pairMeta('BASE_BREAKDOWN', 'sell')),
},
CLOUD_BREAKDOWN: {
sell: def => wrapOr([
makeIch(def, 'CLOUD_BREAK_DOWN', 'CLOSE_PRICE', 'NONE'),
], pairMeta('CLOUD_BREAKDOWN', 'sell')),
},
LAGGING_BEAR: {
sell: def => wrapOr([
makeIch(def, 'LAGGING_LT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE'),
], pairMeta('LAGGING_BEAR', 'sell')),
},
CLOUD_RESISTANCE: {
sell: def => wrapAnd([
makeIch(def, 'LT', 'CLOSE_PRICE', 'LEADING_SPAN1'),
makeIch(def, 'LT', 'CLOSE_PRICE', 'BASE_LINE'),
], pairMeta('CLOUD_RESISTANCE', 'sell')),
},
SANYAKU_REVERSE: {
sell: def => wrapAnd([
makeIch(def, 'LT', 'CONVERSION_LINE', 'BASE_LINE'),
makeIch(def, 'BELOW_CLOUD', 'CLOSE_PRICE', 'NONE'),
makeIch(def, 'LAGGING_LT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE'),
], pairMeta('SANYAKU_REVERSE', 'sell')),
},
GYEOKJEON: {
sell: def => wrapAnd([
makeIch(def, 'LT', 'CONVERSION_LINE', 'BASE_LINE'),
makeIch(def, 'BELOW_CLOUD', 'CLOSE_PRICE', 'NONE'),
makeIch(def, 'SPAN1_LT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2'),
makeIch(def, 'LAGGING_LT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE'),
], pairMeta('GYEOKJEON', 'sell')),
},
EW1: {
buy: def => wrapAnd([
makeIch(def, 'CROSS_UP', 'CLOSE_PRICE', 'BASE_LINE'),
makeIch(def, 'GT', 'CONVERSION_LINE', 'BASE_LINE'),
], pairMeta('EW1', 'buy')),
},
EW2: {
buy: def => wrapAnd([
makeIch(def, 'GT', 'CLOSE_PRICE', 'BASE_LINE'),
makeIch(def, 'GT', 'CLOSE_PRICE', 'LEADING_SPAN2'),
makeIch(def, 'GT', 'CONVERSION_LINE', 'BASE_LINE'),
], pairMeta('EW2', 'buy')),
sell: def => wrapOr([
makeIch(def, 'CROSS_DOWN', 'CLOSE_PRICE', 'BASE_LINE'),
makeIch(def, 'CLOUD_BREAK_DOWN', 'CLOSE_PRICE', 'NONE'),
], pairMeta('EW2', 'sell')),
},
EW3: {
buy: def => wrapAnd([
makeIch(def, 'GT', 'CONVERSION_LINE', 'BASE_LINE'),
makeIch(def, 'ABOVE_CLOUD', 'CLOSE_PRICE', 'NONE'),
makeIch(def, 'LAGGING_GT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE'),
makeIch(def, 'SPAN1_GT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2'),
], pairMeta('EW3', 'buy')),
},
EW4: {
buy: def => wrapAnd([
makeIch(def, 'GT', 'CLOSE_PRICE', 'LEADING_SPAN1'),
makeIch(def, 'GT', 'CONVERSION_LINE', 'BASE_LINE'),
], pairMeta('EW4', 'buy')),
sell: def => wrapOr([
makeIch(def, 'CROSS_DOWN', 'CLOSE_PRICE', 'BASE_LINE'),
makeIch(def, 'CLOUD_BREAK_DOWN', 'CLOSE_PRICE', 'NONE'),
], pairMeta('EW4', 'sell')),
},
EW5: {
sell: def => wrapOr([
makeIch(def, 'CROSS_DOWN', 'CLOSE_PRICE', 'CONVERSION_LINE'),
makeIch(def, 'CROSS_DOWN', 'CONVERSION_LINE', 'BASE_LINE'),
makeIch(def, 'CLOUD_BREAK_DOWN', 'CLOSE_PRICE', 'NONE'),
], pairMeta('EW5', 'sell')),
},
EW_A: {
sell: def => wrapOr([
makeIch(def, 'CROSS_DOWN', 'CLOSE_PRICE', 'BASE_LINE'),
makeIch(def, 'CROSS_DOWN', 'CONVERSION_LINE', 'BASE_LINE'),
], pairMeta('EW_A', 'sell')),
},
EW_B: {
sell: def => wrapAnd([
makeIch(def, 'LT', 'CLOSE_PRICE', 'BASE_LINE'),
makeIch(def, 'BELOW_CLOUD', 'CLOSE_PRICE', 'NONE'),
makeIch(def, 'LT', 'CONVERSION_LINE', 'BASE_LINE'),
], pairMeta('EW_B', 'sell')),
},
EW_C: {
sell: def => wrapAnd([
makeIch(def, 'CLOUD_BREAK_DOWN', 'CLOSE_PRICE', 'NONE'),
makeIch(def, 'BELOW_CLOUD', 'CLOSE_PRICE', 'NONE'),
makeIch(def, 'LT', 'CONVERSION_LINE', 'BASE_LINE'),
makeIch(def, 'LAGGING_LT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE'),
], pairMeta('EW_C', 'sell')),
},
};
export function ichimokuSituationMetaFromValue(value: string): { situationId: IchimokuSituationId; mode: 'buy' | 'sell' } | null {
return VALUE_TO_META[value] ?? null;
}
export function buildIchimokuSituationTree(value: string, def: IndicatorPeriodDef): LogicNode | null {
const meta = ichimokuSituationMetaFromValue(value);
if (!meta) return null;
const builder = BUILDERS[meta.situationId][meta.mode];
return builder ? builder(def) : null;
}
export function ichimokuSituationDisplayName(situationId: IchimokuSituationId, mode: 'buy' | 'sell'): string {
const item = ICHIMOKU_SITUATION_PALETTE_ITEMS.find(
i => i.situationId === situationId && i.mode === mode,
);
return item?.label ?? `${situationId} ${mode === 'buy' ? '매수' : '매도'}`;
}
export function ichimokuSituationPaletteDesc(value: string): string {
return ICHIMOKU_SITUATION_PALETTE_ITEMS.find(i => i.value === value)?.desc ?? '';
}
const COND_LABEL: Record<string, string> = {
CROSS_UP: '상향돌파', CROSS_DOWN: '하향돌파',
GT: '>', LT: '<',
ABOVE_CLOUD: '구름 위', BELOW_CLOUD: '구름 아래',
CLOUD_BREAK_UP: '구름 상향돌파', CLOUD_BREAK_DOWN: '구름 하향이탈',
SPAN1_GT_SPAN2: '양운', SPAN1_LT_SPAN2: '음운',
LAGGING_GT_PRICE: '후행>26봉전 종가', LAGGING_LT_PRICE: '후행<26봉전 종가',
};
function fmtNode(n: LogicNode): string {
if (n.type === 'CONDITION' && n.condition) {
const c = n.condition;
const op = COND_LABEL[c.conditionType] ?? c.conditionType;
return `${c.leftField} ${op} ${c.rightField ?? ''}`.trim();
}
if (n.type === 'AND' || n.type === 'OR') {
return (n.children ?? []).map(fmtNode).join(` ${n.type} `);
}
return '';
}
export function ichimokuSituationSummaryText(
situationId: IchimokuSituationId,
mode: 'buy' | 'sell',
def: IndicatorPeriodDef = {
rsiPeriod: 14, cciPeriod: 20, adxPeriod: 14, williamsR: 14,
trixPeriod: 12, vrPeriod: 26, psyPeriod: 12, newPsy: 12, investPsy: 12,
},
): string {
const tree = BUILDERS[situationId][mode]?.(def);
return tree ? fmtNode(tree) : '';
}
export function ichimokuSituationCategoryLabel(cat: IchimokuSituationCategory): string {
switch (cat) {
case 'basic': return '기본';
case 'alignment': return '호전·역전';
case 'elliott': return '엘리어트';
}
}
@@ -63,6 +63,14 @@ import {
stableStrategyFilterLevelLabel,
type StableStrategyId,
} from '../utils/stableStrategyPairs';
import {
buildIchimokuSituationTree,
isIchimokuSituationPairRoot,
isIchimokuSituationPaletteValue,
ichimokuSituationDisplayName,
ichimokuSituationSummaryText,
type IchimokuSituationId,
} from '../utils/ichimokuSituationStrategy';
import {
buildPriceExtremeBreakoutTree,
isPriceExtremeBreakoutPairPaletteValue,
@@ -1015,6 +1023,10 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
const cfg = inferIchimokuBbConfig(node);
return `${ichimokuBbDisplayName(cfg.mode, cfg.chikouDisplacement)}\n${ichimokuBbSummaryText(cfg)}`;
}
if (isIchimokuSituationPairRoot(node) && node.ichimokuSituationPair) {
const { situationId, mode } = node.ichimokuSituationPair;
return `${ichimokuSituationDisplayName(situationId as IchimokuSituationId, mode)}\n${ichimokuSituationSummaryText(situationId as IchimokuSituationId, mode, DEF)}`;
}
if (isPriceExtremeBreakoutPairRoot(node) && node.priceExtremePair) {
const { mode, shortPeriod, longPeriod } = node.priceExtremePair;
const level = inferPriceExtremeFilterLevel(node);
@@ -1038,6 +1050,10 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
const cfg = inferIchimokuBbConfig(node);
return `${ichimokuBbDisplayName(cfg.mode, cfg.chikouDisplacement)}\n${ichimokuBbSummaryText(cfg)}`;
}
if (isIchimokuSituationPairRoot(node) && node.ichimokuSituationPair) {
const { situationId, mode } = node.ichimokuSituationPair;
return `${ichimokuSituationDisplayName(situationId as IchimokuSituationId, mode)}\n${ichimokuSituationSummaryText(situationId as IchimokuSituationId, mode, DEF)}`;
}
if (isStochOverboughtPairRoot(node)) {
const sec = node.stochPair!.secondaryIndicatorType;
return `${stochPairDisplayName(sec)}\n${stochPairSummaryText(sec)}`;
@@ -1565,6 +1581,8 @@ export type MakeNodeOptions = {
ichimokuBb?: boolean;
/** 추천 안정형 전략 복합 */
stableStrategy?: boolean;
/** 일목균형표 상황별·엘리어트 복합 */
ichimokuSituation?: boolean;
};
/** 팔레트 드래그 payload → makeNode 옵션 */
@@ -1575,6 +1593,7 @@ export function makeNodeOptionsFromPalette(data: {
inflection33?: boolean;
ichimokuBb?: boolean;
stableStrategy?: boolean;
ichimokuSituation?: boolean;
secondaryIndicator?: string;
period?: number;
shortPeriod?: number;
@@ -1601,6 +1620,9 @@ export function makeNodeOptionsFromPalette(data: {
if (data.stableStrategy || (data.value && isStableStrategyPaletteValue(data.value))) {
return { stableStrategy: true };
}
if (data.ichimokuSituation || (data.value && isIchimokuSituationPaletteValue(data.value))) {
return { ichimokuSituation: true };
}
if (data.composite) {
return {
composite: true,
@@ -1638,6 +1660,10 @@ export const makeNode = (
const tree = buildStableStrategyTree(value, DEF);
if (tree) return tree;
}
if (options?.ichimokuSituation || isIchimokuSituationPaletteValue(value)) {
const tree = buildIchimokuSituationTree(value, DEF);
if (tree) return tree;
}
if (options?.composite) {
let condition = makeCompositeCondition(value, signalType, DEF);
condition = {
@@ -12,6 +12,7 @@ import {
isNewLow920SellPaletteValue,
} from './priceExtremeBreakoutPair';
import { STABLE_STRATEGY_PALETTE_ITEMS } from './stableStrategyPairs';
import { ichimokuSituationMetaFromValue } from './ichimokuSituationStrategy';
export type TemplatePaletteSource =
| 'user'
@@ -50,6 +51,8 @@ export function inferCompositePaletteSignal(item: PaletteItem): 'buy' | 'sell' {
}
const stable = STABLE_STRATEGY_PALETTE_ITEMS.find(s => s.value === item.value);
if (stable) return stable.mode;
const ichimokuSit = ichimokuSituationMetaFromValue(item.value);
if (ichimokuSit) return ichimokuSit.mode;
if (item.value.endsWith('_BUY')) return 'buy';
if (item.value.endsWith('_SELL')) return 'sell';
return 'buy';
@@ -26,6 +26,10 @@ import {
buildStableStrategyTree,
isStableStrategyPaletteValue,
} from './stableStrategyPairs';
import {
buildIchimokuSituationTree,
isIchimokuSituationPaletteValue,
} from './ichimokuSituationStrategy';
import {
buildSidewaysFilterNode,
loadSidewaysPaletteItems,
@@ -71,18 +75,21 @@ function buildLogicNodeFromPaletteItem(
const inflection33 = isInflection33PaletteValue(item.value);
const ichimokuBb = isIchimokuBbPaletteValue(item.value);
const stableStrategy = isStableStrategyPaletteValue(item.value);
const ichimokuSituation = isIchimokuSituationPaletteValue(item.value);
const composite = item.kind === 'composite'
&& !stochPair
&& !priceExtremeBreakout
&& !inflection33
&& !ichimokuBb
&& !stableStrategy;
&& !stableStrategy
&& !ichimokuSituation;
if (stochPair) return buildStochOverboughtPairTree(item.secondaryIndicator ?? 'CCI', def);
if (priceExtremeBreakout) return buildPriceExtremeBreakoutTree(item.value, def);
if (inflection33) return buildInflection33Tree(item.value, def);
if (ichimokuBb) return buildIchimokuBbTree(item.value, def);
if (stableStrategy) return buildStableStrategyTree(item.value, def);
if (ichimokuSituation) return buildIchimokuSituationTree(item.value, def);
return makeNode('indicator', item.value, signalTab, def, composite ? { composite: true } : undefined);
}
@@ -18,6 +18,9 @@ import {
import {
STABLE_STRATEGY_PALETTE_ITEMS,
} from './stableStrategyPairs';
import {
ICHIMOKU_SITUATION_PALETTE_ITEMS,
} from './ichimokuSituationStrategy';
import {
NEW_HIGH_9_20_BUY_VALUE,
NEW_LOW_9_20_SELL_VALUE,
@@ -125,6 +128,12 @@ export const DEFAULT_COMPOSITE_ITEMS: Omit<PaletteItem, 'id' | 'kind'>[] = [
builtIn: true,
period: 26,
},
...ICHIMOKU_SITUATION_PALETTE_ITEMS.map(i => ({
value: i.value,
label: i.label,
desc: `[${i.category === 'basic' ? '기본' : i.category === 'alignment' ? '호전·역전' : '엘리어트'}] ${i.desc}`,
builtIn: true,
})),
...STABLE_STRATEGY_PALETTE_ITEMS.map(i => ({
value: i.value,
label: i.label,
+5
View File
@@ -80,6 +80,11 @@ export interface LogicNode {
bbBand: 'UPPER_BAND' | 'MIDDLE_BAND' | 'LOWER_BAND';
conditionType: 'CROSS_UP' | 'CROSS_DOWN' | 'GT' | 'LT';
};
/** 일목균형표 상황별·엘리어트 파동 복합 — @see 일목균형표 전략.md */
ichimokuSituationPair?: {
situationId: string;
mode: 'buy' | 'sell';
};
}
export interface StrategyDSLDto {