diff --git a/frontend/src/utils/inflection33Strategy.ts b/frontend/src/utils/inflection33Strategy.ts index b8ae562..a613cd6 100644 --- a/frontend/src/utils/inflection33Strategy.ts +++ b/frontend/src/utils/inflection33Strategy.ts @@ -16,7 +16,7 @@ export const DEFAULT_EMA_SLOPE_BARS = 2; /** 필터 강도 — 전략편집기에서 조절 */ export type Inflection33FilterLevel = 'strict' | 'balanced' | 'relaxed'; -export const DEFAULT_INFLECTION33_FILTER_LEVEL: Inflection33FilterLevel = 'balanced'; +export const DEFAULT_INFLECTION33_FILTER_LEVEL: Inflection33FilterLevel = 'relaxed'; export const INFLECTION_33_FILTER_OPTIONS: { value: Inflection33FilterLevel; @@ -30,13 +30,13 @@ export const INFLECTION_33_FILTER_OPTIONS: { }, { value: 'balanced', - label: '보통 (권장)', - desc: 'EMA 2봉 변곡 + 종가 EMA 필터 — 기본 33변곡', + label: '보통', + desc: 'EMA 2봉 변곡 + 33봉 채널 돌파 — 종가 EMA 필터 없음', }, { value: 'relaxed', - label: '완화', - desc: 'EMA 1봉 변곡, 종가 EMA 필터 생략 — 매수 빈도↑, 매도는 채널 이탈만', + label: '완화 (권장)', + desc: 'EMA 1봉 변곡 + 채널 돌파 — 매수 빈도↑, 매도는 채널 이탈만', }, ]; @@ -64,7 +64,7 @@ const FILTER_PRESETS: Record = { }, balanced: { slopeBars: 2, - requireCloseAboveEma: true, + requireCloseAboveEma: false, buyAdx: null, sellAdx: null, volume: false, @@ -428,10 +428,10 @@ export function inflection33SummaryText( export function inflection33PaletteDesc(value: string): string { if (isInflection33BuyPaletteValue(value)) { - return '33 EMA 상승 변곡 + 종가 채널 돌파 + 조절 가능 필터 (기본: 보통)'; + return '33 EMA 상승 변곡 + 종가 채널 돌파 + 조절 가능 필터 (기본: 완화)'; } if (isInflection33SellPaletteValue(value)) { - return '33 EMA 하락 이탈 OR 종가 채널 하향 이탈 + 조절 가능 필터 (기본: 보통)'; + return '33 EMA 하락 이탈 OR 종가 채널 하향 이탈 + 조절 가능 필터 (기본: 완화)'; } return ''; } diff --git a/frontend/src/utils/priceExtremeBreakoutPair.ts b/frontend/src/utils/priceExtremeBreakoutPair.ts index 6484fe4..99acf2d 100644 --- a/frontend/src/utils/priceExtremeBreakoutPair.ts +++ b/frontend/src/utils/priceExtremeBreakoutPair.ts @@ -15,7 +15,7 @@ export const DEFAULT_LONG_PERIOD = 20; /** 필터 강도 — 전략편집기에서 조절 */ export type PriceExtremeFilterLevel = 'strict' | 'balanced' | 'relaxed'; -export const DEFAULT_FILTER_LEVEL: PriceExtremeFilterLevel = 'balanced'; +export const DEFAULT_FILTER_LEVEL: PriceExtremeFilterLevel = 'relaxed'; export const PRICE_EXTREME_FILTER_OPTIONS: { value: PriceExtremeFilterLevel; @@ -29,13 +29,13 @@ export const PRICE_EXTREME_FILTER_OPTIONS: { }, { value: 'balanced', - label: '보통 (권장)', - desc: 'ADX·거래량 완화 — 돌파 신호와 균형', + label: '보통', + desc: '20일 돌파 + ADX(≥18) — 가격·추세 2조건', }, { value: 'relaxed', - label: '완화', - desc: '가격 돌파만 — ADX·거래량 필터 없음', + label: '완화 (권장)', + desc: '20일 돌파만 — ADX·거래량·고저가 확인 없음', }, ]; @@ -50,7 +50,7 @@ type FilterPreset = { const FILTER_PRESETS: Record = { strict: { buyAdx: 25, sellAdx: 20, volume: true, shortCrossConfirm: true, highLowConfirm: true }, - balanced: { buyAdx: 20, sellAdx: 15, volume: true, shortCrossConfirm: false, highLowConfirm: true }, + balanced: { buyAdx: 18, sellAdx: 15, volume: false, shortCrossConfirm: false, highLowConfirm: false }, relaxed: { buyAdx: null, sellAdx: null, volume: false, shortCrossConfirm: false, highLowConfirm: false }, }; @@ -324,10 +324,10 @@ export function priceExtremePairSummaryText( export function priceExtremePairPaletteDesc(value: string): string { if (isNewHigh920BuyPaletteValue(value)) { - return '9·20일 신고가 돌파 + 조절 가능 ADX·거래량 필터 (기본: 보통)'; + return '9·20일 신고가 돌파 + 조절 가능 ADX·거래량 필터 (기본: 완화)'; } if (isNewLow920SellPaletteValue(value)) { - return '9·20일 신저가 이탈 + 조절 가능 ADX·거래량 필터 (기본: 보통)'; + return '9·20일 신저가 이탈 + 조절 가능 ADX·거래량 필터 (기본: 완화)'; } return ''; } diff --git a/frontend/src/utils/stableStrategyPairs.ts b/frontend/src/utils/stableStrategyPairs.ts index 4585cd9..84e2825 100644 --- a/frontend/src/utils/stableStrategyPairs.ts +++ b/frontend/src/utils/stableStrategyPairs.ts @@ -7,7 +7,7 @@ import { genId } from './strategyNodeIds'; import { donchianUpperField, donchianLowerField } from './compositeIndicators'; export type StableStrategyFilterLevel = 'strict' | 'balanced' | 'relaxed'; -export const DEFAULT_STABLE_FILTER_LEVEL: StableStrategyFilterLevel = 'balanced'; +export const DEFAULT_STABLE_FILTER_LEVEL: StableStrategyFilterLevel = 'relaxed'; export type StableStrategyId = | 'TURTLE_S1' @@ -26,15 +26,15 @@ export const STABLE_STRATEGY_FILTER_OPTIONS: { desc: string; }[] = [ { value: 'strict', label: '강함', desc: 'ADX·거래량 등 필터 최대 — 신호 적음·품질↑' }, - { value: 'balanced', label: '보통 (권장)', desc: '기본 추천 조합' }, - { value: 'relaxed', label: '완화', desc: '핵심 조건만 — 신호 빈도↑' }, + { value: 'balanced', label: '보통', desc: '핵심 + 보조 1~2조건 — 신호와 품질 균형' }, + { value: 'relaxed', label: '완화 (권장)', desc: '핵심 조건만 — 신호 빈도↑' }, ]; type FilterFlags = { adx: number | null; volume: boolean }; const FILTER_FLAGS: Record = { strict: { adx: 25, volume: true }, - balanced: { adx: 20, volume: false }, + balanced: { adx: null, volume: false }, relaxed: { adx: null, volume: false }, }; @@ -148,7 +148,7 @@ export const STABLE_STRATEGY_PALETTE_ITEMS: StableStrategyPaletteMeta[] = [ }, { value: ICHIMOKU_SANYAKU_BUY, label: '일목 삼역호전 매수', strategyId: 'ICHIMOKU_SANYAKU', mode: 'buy', - desc: '구름 돌파(현재봉) + 보조필터 N봉 내 존재 — 전후 봉 분산 허용, 트리거는 1회', + desc: '구름 돌파(현재봉) + 종가>전환(보통) — 필터 강도에 따라 전환>기준·후행 추가', periodHint: '9 / 26 / 52', }, { @@ -158,17 +158,17 @@ export const STABLE_STRATEGY_PALETTE_ITEMS: StableStrategyPaletteMeta[] = [ }, { value: ICHIMOKU_PERFECT_BUY, label: '일목 5원소 매수', strategyId: 'ICHIMOKU_PERFECT', mode: 'buy', - desc: '괘·주가·구름·후행 4중 AND — 전환>기준 + 구름 위 + 양운 + 후행>26봉전 종가', + desc: '괘·주가(구름) — 강함에서만 양운·후행 4중 AND', periodHint: '9 / 26 / 52', }, { value: ICHIMOKU_PERFECT_SELL, label: '일목 5원소 매도', strategyId: 'ICHIMOKU_PERFECT', mode: 'sell', - desc: '괘·주가·구름·후행 4중 AND — 전환<기준 + 구름 아래 + 음운 + 후행<26봉전 종가', + desc: '괘·주가(구름) — 강함에서만 음운·후행 4중 AND', periodHint: '9 / 26 / 52', }, { value: MACD_MOMENTUM_BUY, label: 'MACD 모멘텀 매수', strategyId: 'MACD_MOMENTUM', mode: 'buy', - desc: 'MACD 상향돌파 + EMA60 위 + ADX', + desc: 'MACD 상향돌파 — 보통: EMA60 위, 강함: +ADX·거래량', periodHint: 'MACD + EMA60', }, { @@ -369,7 +369,7 @@ function buildMaTrendBuy(def: IndicatorPeriodDef, level: StableStrategyFilterLev const nodes: LogicNode[] = [ makeCondition('MA', 'CROSS_UP', maField(short), maField(long), def), ]; - if (level !== 'relaxed') { + if (level === 'strict') { nodes.push(...appendTrendFilters([], level, def, 'buy')); } return wrapAnd(nodes, pairMeta('MA_TREND', 'buy', level)); @@ -450,6 +450,12 @@ function ichimokuSanyakuCoreFilters(def: IndicatorPeriodDef, level: StableStrate if (level === 'relaxed') { return [makeCondition('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'CONVERSION_LINE', def)]; } + if (level === 'balanced') { + return [ + makeCondition('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'CONVERSION_LINE', def), + makeExistsIn('ICHIMOKU', 'GT', 'CONVERSION_LINE', 'BASE_LINE', def, w), + ]; + } const nodes: LogicNode[] = [ makeExistsIn('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'CONVERSION_LINE', def, w), makeExistsIn('ICHIMOKU', 'LAGGING_GT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE', def, w), @@ -496,7 +502,7 @@ function buildIchimokuPerfectBuy(def: IndicatorPeriodDef, level: StableStrategyF makeCondition('ICHIMOKU', 'GT', 'CONVERSION_LINE', 'BASE_LINE', def), makeCondition('ICHIMOKU', 'ABOVE_CLOUD', 'CLOSE_PRICE', 'NONE', def), ]; - if (level !== 'relaxed') { + if (level === 'strict') { nodes.push(makeCondition('ICHIMOKU', 'SPAN1_GT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2', def)); nodes.push(makeCondition('ICHIMOKU', 'LAGGING_GT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE', def)); } @@ -508,7 +514,7 @@ function buildIchimokuPerfectSell(def: IndicatorPeriodDef, level: StableStrategy makeCondition('ICHIMOKU', 'LT', 'CONVERSION_LINE', 'BASE_LINE', def), makeCondition('ICHIMOKU', 'BELOW_CLOUD', 'CLOSE_PRICE', 'NONE', def), ]; - if (level !== 'relaxed') { + if (level === 'strict') { nodes.push(makeCondition('ICHIMOKU', 'SPAN1_LT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2', def)); nodes.push(makeCondition('ICHIMOKU', 'LAGGING_LT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE', def)); } @@ -519,8 +525,10 @@ function buildMacdMomentumBuy(def: IndicatorPeriodDef, level: StableStrategyFilt const nodes: LogicNode[] = [ makeCondition('MACD', 'CROSS_UP', 'MACD_LINE', 'SIGNAL_LINE', def), ]; - if (level !== 'relaxed') { + if (level === 'balanced' || level === 'strict') { nodes.push(makeCondition('EMA', 'GT', 'CLOSE_PRICE', emaField(60), def)); + } + if (level === 'strict') { nodes.push(...appendTrendFilters([], level, def, 'buy')); } return wrapAnd(nodes, pairMeta('MACD_MOMENTUM', 'buy', level)); @@ -637,15 +645,19 @@ export function stableStrategyFilterSummary( if (strategyId === 'ICHIMOKU_SANYAKU') { const w = ichimokuSanyakuWindow(level); if (level === 'strict') parts.push(`돌파1회·필터N${w}·MA60·거래량`); - else if (level === 'balanced') parts.push(`돌파1회·필터N${w}·(전환>기준 OR 양운)`); + else if (level === 'balanced') parts.push(`돌파1회·종가>전환·전환>기준(N${w})`); else parts.push('돌파1회·종가>전환'); } if (strategyId === 'ICHIMOKU_PERFECT') { - if (level === 'relaxed') parts.push('괘·주가(구름) 2조건만'); - else parts.push('괘·주가·구름·후행 4중 AND'); + if (level === 'strict') parts.push('괘·주가·구름·후행 4중 AND'); + else parts.push('괘·주가(구름) 2조건'); } - if (strategyId === 'MACD_MOMENTUM' && level === 'relaxed') parts.push('EMA60·ADX 없음'); - if (strategyId === 'MA_TREND' && level === 'relaxed') parts.push('ADX 없음'); + if (strategyId === 'MACD_MOMENTUM') { + if (level === 'relaxed') parts.push('MACD만'); + else if (level === 'balanced') parts.push('MACD + EMA60'); + else parts.push('MACD + EMA60 + ADX·거래량'); + } + if (strategyId === 'MA_TREND' && level !== 'strict') parts.push('ADX 없음'); return parts.join(', '); } @@ -682,19 +694,28 @@ export function inferStableStrategyFilterLevel(node: LogicNode): StableStrategyF const hasVolRatio = conds.some(c => c.conditionType === 'VOLUME_GT_MA_RATIO'); const hasMa = conds.some(c => c.indicatorType === 'MA' && c.rightField === maField(60)); const hasLagging = conds.some(c => c.conditionType === 'LAGGING_GT_PRICE'); + const hasTenkanKijun = conds.some(c => + c.conditionType === 'GT' && c.leftField === 'CONVERSION_LINE' && c.rightField === 'BASE_LINE', + ); if (hasVolRatio || hasLaggingCloud) return 'strict'; if (hasMa) return 'strict'; - if (hasLagging && (hasCloudBreak || hasChikouCross) && hasWindow) return 'balanced'; - if (hasLagging && (hasCloudBreak || hasChikouCross)) return 'balanced'; + if (hasLagging && hasWindow) return 'strict'; + if (hasTenkanKijun && hasWindow && !hasLagging) 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 (hasSpan && hasLagging) return 'strict'; + return 'relaxed'; + } + if (id === 'MACD_MOMENTUM') { + const hasEma = conds.some(c => c.rightField === emaField(60)); + const hasAdx = conds.some(c => c.indicatorType === 'ADX'); + if (hasAdx) return 'strict'; + if (hasEma) return 'balanced'; + return 'relaxed'; } - 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') { const adxLte = conds.find(c => c.indicatorType === 'ADX' && c.conditionType === 'LTE'); diff --git a/frontend/src/utils/strategyPresets.ts b/frontend/src/utils/strategyPresets.ts index 8375a81..defba3b 100644 --- a/frontend/src/utils/strategyPresets.ts +++ b/frontend/src/utils/strategyPresets.ts @@ -118,7 +118,7 @@ export function getStrategyTemplates(def: DefType): StrategyTemplateDef[] { { kind: 'simple', ind: 'BOLLINGER', cond: 'CROSS_UP', l: 'CLOSE_PRICE', r: 'LOWER_BAND', signal: 'buy', label: '볼린저 하단 돌파 매수' }, { kind: 'simple', ind: 'BOLLINGER', cond: 'CROSS_UP', l: 'CLOSE_PRICE', r: 'UPPER_BAND', signal: 'sell', label: '볼린저 상단 돌파 매도' }, { kind: 'simple', ind: 'CCI', cond: 'CROSS_UP', l: 'CCI_VALUE', r: 'OVERSOLD_NEG100', signal: 'buy', label: 'CCI 과매도 반등' }, - { kind: 'simple', ind: 'PSYCHOLOGICAL', cond: 'CROSS_UP', l: 'PSY_VALUE', r: 'OVERBOUGHT_75', signal: 'buy', label: '심리도 상향 돌파' }, + { kind: 'simple', ind: 'PSYCHOLOGICAL', cond: 'CROSS_UP', l: 'PSY_VALUE', r: 'OVERSOLD_25', signal: 'buy', label: '심리도 과매도 반등 매수' }, { kind: 'simple', ind: 'STOCHASTIC', cond: 'CROSS_UP', l: 'STOCH_K', r: 'STOCH_D', signal: 'buy', label: '스토캐스틱 골든크로스' }, { kind: 'simple', ind: 'ADX', cond: 'CROSS_UP', l: 'ADX_VALUE', r: 'ADX_25', signal: 'buy', label: 'ADX 중앙선 상향 돌파' }, { @@ -169,21 +169,21 @@ export function getStrategyTemplates(def: DefType): StrategyTemplateDef[] { kind: 'composite', signal: 'buy', label: '9·20일 신고가 매수', - description: '9·20일 신고가 돌파 + ADX·거래량 필터 (기본: 보통)', + description: '20일 신고가 돌파 (기본: 완화 — ADX·거래량 없음)', build: () => buildNewHigh920BuyTree(def), }, { kind: 'composite', signal: 'sell', label: '9·20일 신저가 매도', - description: '9·20일 신저가 이탈 + ADX·거래량 필터 (기본: 보통)', + description: '20일 신저가 이탈 (기본: 완화 — ADX·거래량 없음)', build: () => buildNewLow920SellTree(def), }, { kind: 'composite', signal: 'buy', label: '33변곡 매수', - description: '33 EMA 상승 변곡 + 종가 EMA 위 + 33봉 종가 채널 상향 돌파', + description: '33 EMA 상승 변곡 + 33봉 종가 채널 상향 돌파 (기본: 완화)', build: () => build33InflectionBuyTree(def), }, {