전략편집기 횡보구간 컨트롤 추가
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* 횡보(레인지) 필터 팔레트 — 전략편집기 우측 「횡보필터」 탭
|
||||
* 각 항목은 LogicNode(조건 또는 AND/NOT 조합)를 생성합니다.
|
||||
*/
|
||||
import type { LogicNode } from './strategyTypes';
|
||||
import type { DefType } from './strategyEditorShared';
|
||||
import { genId } from './strategyNodeIds';
|
||||
import { THRESHOLD_HL_MID, THRESHOLD_HL_UNDER } from './thresholdSymbols';
|
||||
|
||||
export type SidewaysFilterMode = 'include' | 'exclude';
|
||||
|
||||
export type SidewaysFilterCategory =
|
||||
| 'adx'
|
||||
| 'bollinger'
|
||||
| 'ma'
|
||||
| 'ichimoku'
|
||||
| 'donchian'
|
||||
| 'oscillator'
|
||||
| 'dmi'
|
||||
| 'volume'
|
||||
| 'combo';
|
||||
|
||||
export interface SidewaysFilterItem {
|
||||
id: string;
|
||||
label: string;
|
||||
desc: string;
|
||||
category: SidewaysFilterCategory;
|
||||
/** include=횡보 구간만, exclude=횡보 제외(추세만) */
|
||||
mode: SidewaysFilterMode;
|
||||
build: (def: DefType) => LogicNode;
|
||||
}
|
||||
|
||||
export const SIDEWAYS_FILTER_CATEGORY_LABEL: Record<SidewaysFilterCategory, string> = {
|
||||
adx: 'ADX · 추세강도',
|
||||
bollinger: '볼린저 밴드',
|
||||
ma: 'MA · 이동평균 수렴',
|
||||
ichimoku: '일목균형표',
|
||||
donchian: 'Donchian · 채널',
|
||||
oscillator: '오실레이터 중립',
|
||||
dmi: 'DMI · 방향성',
|
||||
volume: '거래량 · VR',
|
||||
combo: '복합 횡보 필터',
|
||||
};
|
||||
|
||||
function condition(
|
||||
indicatorType: string,
|
||||
conditionType: string,
|
||||
leftField: string,
|
||||
rightField: string,
|
||||
extra?: Partial<NonNullable<LogicNode['condition']>>,
|
||||
): LogicNode {
|
||||
return {
|
||||
id: genId(),
|
||||
type: 'CONDITION',
|
||||
condition: {
|
||||
indicatorType,
|
||||
conditionType,
|
||||
leftField,
|
||||
rightField,
|
||||
candleRange: 1,
|
||||
valuePeriodOverride: false,
|
||||
thresholdOverride: false,
|
||||
rightPeriodOverride: false,
|
||||
...extra,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function andNode(...children: LogicNode[]): LogicNode {
|
||||
return { id: genId(), type: 'AND', children };
|
||||
}
|
||||
|
||||
function orNode(...children: LogicNode[]): LogicNode {
|
||||
return { id: genId(), type: 'OR', children };
|
||||
}
|
||||
|
||||
function notNode(child: LogicNode): LogicNode {
|
||||
return { id: genId(), type: 'NOT', children: [child] };
|
||||
}
|
||||
|
||||
function maField(def: DefType, index: number): string {
|
||||
const p = def.maLines[index] ?? def.maLines[def.maLines.length - 1] ?? 20;
|
||||
return `MA${p}`;
|
||||
}
|
||||
|
||||
function diffCond(
|
||||
ind: string,
|
||||
left: string,
|
||||
right: string,
|
||||
op: 'DIFF_LT' | 'DIFF_GT',
|
||||
compareValue: number,
|
||||
): LogicNode {
|
||||
return condition(ind, op, left, right, { compareValue });
|
||||
}
|
||||
|
||||
function rsiBand(def: DefType, low: number, high: number): LogicNode {
|
||||
return andNode(
|
||||
condition('RSI', 'GTE', 'RSI_VALUE', `K_${low}`, { compareValue: low }),
|
||||
condition('RSI', 'LTE', 'RSI_VALUE', `K_${high}`, { compareValue: high }),
|
||||
);
|
||||
}
|
||||
|
||||
function stochBand(low: number, high: number): LogicNode {
|
||||
return andNode(
|
||||
condition('STOCHASTIC', 'GTE', 'STOCH_K', `K_${low}`),
|
||||
condition('STOCHASTIC', 'LTE', 'STOCH_K', `K_${high}`),
|
||||
);
|
||||
}
|
||||
|
||||
function cciBand(low: number, high: number): LogicNode {
|
||||
return andNode(
|
||||
condition('CCI', 'GTE', 'CCI_VALUE', `K_${low}`),
|
||||
condition('CCI', 'LTE', 'CCI_VALUE', `K_${high}`),
|
||||
);
|
||||
}
|
||||
|
||||
function adxWeak(): LogicNode {
|
||||
return condition('ADX', 'LT', 'ADX_VALUE', THRESHOLD_HL_MID);
|
||||
}
|
||||
|
||||
function adxStrong(): LogicNode {
|
||||
return condition('ADX', 'GTE', 'ADX_VALUE', THRESHOLD_HL_MID);
|
||||
}
|
||||
|
||||
function adxVeryWeak(): LogicNode {
|
||||
return condition('ADX', 'LT', 'ADX_VALUE', 'ADX_20');
|
||||
}
|
||||
|
||||
function inCloud(): LogicNode {
|
||||
return condition('ICHIMOKU', 'IN_CLOUD', 'CLOSE_PRICE', 'CLOSE_PRICE');
|
||||
}
|
||||
|
||||
function bbSqueeze(def: DefType): LogicNode {
|
||||
void def;
|
||||
return diffCond('BOLLINGER', 'UPPER_BAND', 'LOWER_BAND', 'DIFF_LT', 2);
|
||||
}
|
||||
|
||||
function maConverge(def: DefType, shortIdx: number, longIdx: number, threshold: number): LogicNode {
|
||||
return diffCond('MA', maField(def, shortIdx), maField(def, longIdx), 'DIFF_LT', threshold);
|
||||
}
|
||||
|
||||
function dcNarrow(period: number, threshold: number): LogicNode {
|
||||
return diffCond(
|
||||
'DONCHIAN',
|
||||
`DC_UPPER_${period}`,
|
||||
`DC_LOWER_${period}`,
|
||||
'DIFF_LT',
|
||||
threshold,
|
||||
);
|
||||
}
|
||||
|
||||
function buildInclude(inner: LogicNode): LogicNode {
|
||||
return inner;
|
||||
}
|
||||
|
||||
function buildExclude(inner: LogicNode): LogicNode {
|
||||
return notNode(inner);
|
||||
}
|
||||
|
||||
function item(
|
||||
id: string,
|
||||
label: string,
|
||||
desc: string,
|
||||
category: SidewaysFilterCategory,
|
||||
mode: SidewaysFilterMode,
|
||||
inner: LogicNode | ((def: DefType) => LogicNode),
|
||||
): SidewaysFilterItem {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
desc,
|
||||
category,
|
||||
mode,
|
||||
build: def => {
|
||||
const node = typeof inner === 'function' ? inner(def) : inner;
|
||||
return mode === 'exclude' ? buildExclude(node) : buildInclude(node);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 내장 횡보 필터 목록 */
|
||||
export const DEFAULT_SIDEWAYS_FILTER_ITEMS: SidewaysFilterItem[] = [
|
||||
// ── ADX ──
|
||||
item('adx-lt-25', 'ADX < 25', '평균방향지수 25 미만 — 비추세·횡보', 'adx', 'include', adxWeak()),
|
||||
item('adx-lt-20', 'ADX < 20', 'ADX 20 미만 — 강한 횡보', 'adx', 'include', adxVeryWeak()),
|
||||
item('adx-slope-down', 'ADX 하락 중', '추세 강도가 줄어드는 구간', 'adx', 'include',
|
||||
condition('ADX', 'SLOPE_DOWN', 'ADX_VALUE', 'ADX_VALUE', { slopePeriod: 3 })),
|
||||
item('adx-gte-25', 'ADX ≥ 25', '추세 형성 구간 — 횡보 제외', 'adx', 'exclude', adxWeak()),
|
||||
item('adx-gte-25-direct', 'ADX ≥ 25 (추세)', '추세 강도 25 이상', 'adx', 'include', adxStrong()),
|
||||
item('adx-gte-20', 'ADX ≥ 20', '약한 추세 이상 — 횡보 제외', 'adx', 'exclude', adxVeryWeak()),
|
||||
|
||||
// ── 볼린저 ──
|
||||
item('bb-squeeze', '볼린저 수축', '상·하단밴드 간격 좁음 (Squeeze) — compareValue 조절', 'bollinger', 'include', bbSqueeze),
|
||||
item('bb-wide', '볼린저 확장', '밴드폭 넓음 — 변동성 확대·횡보 제외', 'bollinger', 'exclude', def => bbSqueeze(def)),
|
||||
item('bb-close-near-mid', '종가 ≈ 중심선', '종가가 볼린저 중심선 근처', 'bollinger', 'include',
|
||||
diffCond('BOLLINGER', 'CLOSE_PRICE', 'MIDDLE_BAND', 'DIFF_LT', 1)),
|
||||
item('bb-close-in-band', '종가 밴드 내부', '하단 이상 & 상단 이하 (밴드 안)', 'bollinger', 'include', andNode(
|
||||
condition('BOLLINGER', 'GTE', 'CLOSE_PRICE', 'LOWER_BAND'),
|
||||
condition('BOLLINGER', 'LTE', 'CLOSE_PRICE', 'UPPER_BAND'),
|
||||
)),
|
||||
|
||||
// ── MA ──
|
||||
item('ma-5-20-narrow', 'MA 단·중 수렴', '단기·중기 MA 간격 좁음 — compareValue 조절', 'ma', 'include',
|
||||
def => maConverge(def, 0, 1, 1)),
|
||||
item('ma-5-60-narrow', 'MA 단·장 수렴', '단기·장기 MA 수렴', 'ma', 'include',
|
||||
def => maConverge(def, 0, 3, 2)),
|
||||
item('ma-10-20-narrow', 'MA2·MA3 수렴', '중기 MA 두 선 근접', 'ma', 'include',
|
||||
def => maConverge(def, 1, 2, 1)),
|
||||
item('ma-spread-wide', 'MA 간격 넓음', 'MA 스프레드 큼 — 추세·횡보 제외', 'ma', 'exclude',
|
||||
def => maConverge(def, 0, 2, 5)),
|
||||
|
||||
// ── 일목 ──
|
||||
item('ich-in-cloud', '구름 안', '종가가 일목 구름대 내부', 'ichimoku', 'include', inCloud()),
|
||||
item('ich-not-cloud', '구름 밖', '구름 위 또는 아래 — 횡보 제외', 'ichimoku', 'exclude', inCloud()),
|
||||
item('ich-span-thin', '구름 얇음', '선행1·선행2 간격 좁음', 'ichimoku', 'include',
|
||||
diffCond('ICHIMOKU', 'LEADING_SPAN1', 'LEADING_SPAN2', 'DIFF_LT', 1)),
|
||||
item('ich-above-cloud-excl', '구름 위 (추세)', '강세 구름 위 — 횡보 제외', 'ichimoku', 'exclude', inCloud()),
|
||||
|
||||
// ── Donchian ──
|
||||
item('dc-9-narrow', '9일 채널 좁음', 'Donchian 9일 고저 폭 좁음', 'donchian', 'include', dcNarrow(9, 2)),
|
||||
item('dc-20-narrow', '20일 채널 좁음', '한 달 박스권 — 채널 폭 좁음', 'donchian', 'include', dcNarrow(20, 3)),
|
||||
item('dc-20-wide', '20일 채널 넓음', '채널 확장 — 횡보 제외', 'donchian', 'exclude', dcNarrow(20, 3)),
|
||||
|
||||
// ── 오실레이터 ──
|
||||
item('rsi-40-60', 'RSI 40~60', 'RSI 중립대', 'oscillator', 'include', def => rsiBand(def, 40, 60)),
|
||||
item('rsi-45-55', 'RSI 45~55', 'RSI 좁은 중립대', 'oscillator', 'include', def => rsiBand(def, 45, 55)),
|
||||
item('rsi-not-neutral', 'RSI 중립 아님', 'RSI 40~60 밖 — 횡보 제외', 'oscillator', 'exclude', def => rsiBand(def, 40, 60)),
|
||||
item('stoch-40-60', 'Stoch 40~60', '%K 중립대', 'oscillator', 'include', stochBand(40, 60)),
|
||||
item('stoch-30-70', 'Stoch 30~70', '%K 넓은 중립', 'oscillator', 'include', stochBand(30, 70)),
|
||||
item('cci-neutral', 'CCI -50~+50', 'CCI 중립 박스', 'oscillator', 'include', cciBand(-50, 50)),
|
||||
item('cci-near-zero', 'CCI ≈ 0', 'CCI 0선 근처', 'oscillator', 'include',
|
||||
diffCond('CCI', 'CCI_VALUE', THRESHOLD_HL_MID, 'DIFF_LT', 50)),
|
||||
item('wr-neutral', 'Williams -60~-40', 'Williams %R 중립', 'oscillator', 'include', andNode(
|
||||
condition('WILLIAMS_R', 'GTE', 'WILLIAMS_R_VALUE', 'K_-60'),
|
||||
condition('WILLIAMS_R', 'LTE', 'WILLIAMS_R_VALUE', 'K_-40'),
|
||||
)),
|
||||
item('macd-hist-flat', 'MACD 히스토그램 ≈0', '모멘텀 약함', 'oscillator', 'include',
|
||||
diffCond('MACD', 'HISTOGRAM', 'ZERO_LINE', 'DIFF_LT', 0.5)),
|
||||
item('trix-near-zero', 'TRIX ≈ 0', 'TRIX 0선 근처', 'oscillator', 'include',
|
||||
diffCond('TRIX', 'TRIX_VALUE', 'ZERO_LINE', 'DIFF_LT', 0.1)),
|
||||
item('disp-near-100', '이격도 ≈ 100', '20일 이격도 기준선 근처', 'oscillator', 'include',
|
||||
diffCond('DISPARITY', 'DISPARITY20', THRESHOLD_HL_MID, 'DIFF_LT', 3)),
|
||||
|
||||
// ── DMI ──
|
||||
item('dmi-no-direction', '+DI ≈ -DI', '방향성 우열 없음', 'dmi', 'include',
|
||||
diffCond('DMI', 'PDI', 'MDI', 'DIFF_LT', 5)),
|
||||
item('dmi-both-weak', '+DI·-DI 모두 약함', '양 방향 모두 10 미만', 'dmi', 'include', andNode(
|
||||
condition('DMI', 'LT', 'PDI', THRESHOLD_HL_UNDER),
|
||||
condition('DMI', 'LT', 'MDI', THRESHOLD_HL_UNDER),
|
||||
)),
|
||||
item('dmi-clear-direction', '방향성 뚜렷', '+DI·-DI 차이 큼 — 횡보 제외', 'dmi', 'exclude',
|
||||
diffCond('DMI', 'PDI', 'MDI', 'DIFF_LT', 5)),
|
||||
|
||||
// ── 거래량 ──
|
||||
item('vol-below-ma', '거래량 < MA', '평균 대비 거래량 위축', 'volume', 'include',
|
||||
condition('VOLUME', 'LT', 'VOLUME_VALUE', 'VOLUME_MA')),
|
||||
item('vol-above-ma', '거래량 > MA', '거래량 증가 — 횡보 제외', 'volume', 'exclude',
|
||||
condition('VOLUME', 'LT', 'VOLUME_VALUE', 'VOLUME_MA')),
|
||||
item('vr-near-100', 'VR ≈ 100', 'VR 기준선 근처', 'volume', 'include',
|
||||
diffCond('VR', 'VR_VALUE', THRESHOLD_HL_MID, 'DIFF_LT', 30)),
|
||||
|
||||
// ── 복합 ──
|
||||
item('combo-adx-cloud', 'ADX<25 + 구름안', '비추세 + 일목 구름대', 'combo', 'include', andNode(adxWeak(), inCloud())),
|
||||
item('combo-adx-rsi', 'ADX<25 + RSI중립', '비추세 + RSI 40~60', 'combo', 'include', def => andNode(adxWeak(), rsiBand(def, 40, 60))),
|
||||
item('combo-adx-bb', 'ADX<25 + BB수축', '비추세 + 볼린저 수축', 'combo', 'include', def => andNode(adxWeak(), bbSqueeze(def))),
|
||||
item('combo-adx-ma', 'ADX<25 + MA수렴', '비추세 + MA 단·중 수렴', 'combo', 'include',
|
||||
def => andNode(adxWeak(), maConverge(def, 0, 1, 1))),
|
||||
item('combo-cloud-rsi', '구름안 + RSI중립', '일목 횡보 + RSI 중립', 'combo', 'include', def => andNode(inCloud(), rsiBand(def, 40, 60))),
|
||||
item('combo-triple', 'ADX+구름+RSI', '3중 횡보 확인', 'combo', 'include', def => andNode(adxWeak(), inCloud(), rsiBand(def, 40, 60))),
|
||||
item('combo-loose-range', 'ADX<20 또는 구름안', '느슨한 횡보 OR', 'combo', 'include', orNode(adxVeryWeak(), inCloud())),
|
||||
item('combo-trend-only', '추세만 (3중 제외)', 'NOT(ADX<25 ∧ 구름안 ∧ RSI중립)', 'combo', 'exclude', def =>
|
||||
andNode(adxWeak(), inCloud(), rsiBand(def, 40, 60))),
|
||||
item('combo-adx-dc', 'ADX<25 + 20일채널좁음', '비추세 + Donchian 박스', 'combo', 'include',
|
||||
andNode(adxWeak(), dcNarrow(20, 3))),
|
||||
item('combo-full-range', '풀 횡보 필터', 'ADX+BB수축+MA수렴+RSI', 'combo', 'include', def => andNode(
|
||||
adxWeak(),
|
||||
bbSqueeze(def),
|
||||
maConverge(def, 0, 1, 1),
|
||||
rsiBand(def, 45, 55),
|
||||
)),
|
||||
];
|
||||
|
||||
const FILTER_MAP = new Map(DEFAULT_SIDEWAYS_FILTER_ITEMS.map(i => [i.id, i]));
|
||||
|
||||
export function getSidewaysFilterItem(id: string): SidewaysFilterItem | undefined {
|
||||
return FILTER_MAP.get(id);
|
||||
}
|
||||
|
||||
export function buildSidewaysFilterNode(id: string, def: DefType): LogicNode | null {
|
||||
const item = getSidewaysFilterItem(id);
|
||||
if (!item) return null;
|
||||
return item.build(def);
|
||||
}
|
||||
|
||||
export function filterSidewaysItems(
|
||||
items: SidewaysFilterItem[],
|
||||
query: string,
|
||||
modeFilter: 'all' | SidewaysFilterMode,
|
||||
): SidewaysFilterItem[] {
|
||||
const q = query.trim().toLowerCase();
|
||||
return items.filter(i => {
|
||||
if (modeFilter !== 'all' && i.mode !== modeFilter) return false;
|
||||
if (!q) return true;
|
||||
const cat = SIDEWAYS_FILTER_CATEGORY_LABEL[i.category];
|
||||
return (
|
||||
i.label.toLowerCase().includes(q)
|
||||
|| i.desc.toLowerCase().includes(q)
|
||||
|| i.id.toLowerCase().includes(q)
|
||||
|| cat.toLowerCase().includes(q)
|
||||
|| (i.mode === 'include' ? '횡보' : '제외').includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function groupSidewaysFiltersByCategory(
|
||||
items: SidewaysFilterItem[],
|
||||
): { category: SidewaysFilterCategory; label: string; items: SidewaysFilterItem[] }[] {
|
||||
const order: SidewaysFilterCategory[] = [
|
||||
'adx', 'bollinger', 'ma', 'ichimoku', 'donchian', 'oscillator', 'dmi', 'volume', 'combo',
|
||||
];
|
||||
const grouped = new Map<SidewaysFilterCategory, SidewaysFilterItem[]>();
|
||||
for (const i of items) {
|
||||
const list = grouped.get(i.category) ?? [];
|
||||
list.push(i);
|
||||
grouped.set(i.category, list);
|
||||
}
|
||||
return order
|
||||
.filter(c => grouped.has(c))
|
||||
.map(c => ({
|
||||
category: c,
|
||||
label: SIDEWAYS_FILTER_CATEGORY_LABEL[c],
|
||||
items: grouped.get(c)!,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user