일목균형 전략조건 수정
This commit is contained in:
@@ -143,12 +143,12 @@ export const STABLE_STRATEGY_PALETTE_ITEMS: StableStrategyPaletteMeta[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ICHIMOKU_SANYAKU_BUY, label: '일목 삼역호전 매수', strategyId: 'ICHIMOKU_SANYAKU', mode: 'buy',
|
value: ICHIMOKU_SANYAKU_BUY, label: '일목 삼역호전 매수', strategyId: 'ICHIMOKU_SANYAKU', mode: 'buy',
|
||||||
desc: '구름 상향 돌파 트리거 + 삼역호전 필터 (보통: 후행·양운, 강함: MA·거래량)',
|
desc: '구름 돌파 + 삼역 필터 — 최근 N봉 내 존재로 전후 봉 분산 조건 허용 (보통 20봉)',
|
||||||
periodHint: '9 / 26 / 52',
|
periodHint: '9 / 26 / 52',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ICHIMOKU_SANYAKU_SELL, label: '일목 삼역호전 매도', strategyId: 'ICHIMOKU_SANYAKU', mode: 'sell',
|
value: ICHIMOKU_SANYAKU_SELL, label: '일목 삼역호전 매도', strategyId: 'ICHIMOKU_SANYAKU', mode: 'sell',
|
||||||
desc: '전환·기준·구름 이탈 OR — 돌파 우선, 이탈 유지 fallback',
|
desc: '전환·기준·구름 이탈 — N봉 내 돌파 존재 + 전환선 이탈 fallback',
|
||||||
periodHint: '9 / 26 / 52',
|
periodHint: '9 / 26 / 52',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -228,15 +228,38 @@ function makeCondition(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 최근 N봉(현재 봉 포함) 내 1회 이상 성립 — 전후 봉에 분산된 조건 허용 */
|
||||||
|
function makeExistsIn(
|
||||||
|
indicatorType: string,
|
||||||
|
conditionType: string,
|
||||||
|
leftField: string,
|
||||||
|
rightField: string,
|
||||||
|
def: IndicatorPeriodDef,
|
||||||
|
window: number,
|
||||||
|
extra?: Partial<ConditionDSL>,
|
||||||
|
): LogicNode {
|
||||||
|
return makeCondition(indicatorType, conditionType, leftField, rightField, def, {
|
||||||
|
candleRangeMode: 'EXISTS_IN',
|
||||||
|
candleRange: Math.max(2, window),
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function makeVolumeMaRatioFilter(
|
function makeVolumeMaRatioFilter(
|
||||||
def: IndicatorPeriodDef,
|
def: IndicatorPeriodDef,
|
||||||
ratio: number,
|
ratio: number,
|
||||||
maLength = 5,
|
maLength = 5,
|
||||||
|
window?: number,
|
||||||
): LogicNode {
|
): LogicNode {
|
||||||
return makeCondition('VOLUME', 'VOLUME_GT_MA_RATIO', 'VOLUME_VALUE', 'VOLUME_MA', def, {
|
const extra: Partial<ConditionDSL> = {
|
||||||
compareValue: ratio,
|
compareValue: ratio,
|
||||||
params: { length: maLength },
|
params: { length: maLength },
|
||||||
});
|
};
|
||||||
|
if (window != null) {
|
||||||
|
extra.candleRangeMode = 'EXISTS_IN';
|
||||||
|
extra.candleRange = Math.max(2, window);
|
||||||
|
}
|
||||||
|
return makeCondition('VOLUME', 'VOLUME_GT_MA_RATIO', 'VOLUME_VALUE', 'VOLUME_MA', def, extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeAdxFilter(threshold: number, def: IndicatorPeriodDef): LogicNode {
|
function makeAdxFilter(threshold: number, def: IndicatorPeriodDef): LogicNode {
|
||||||
@@ -371,55 +394,58 @@ function buildIchimokuTrendSell(def: IndicatorPeriodDef, level: StableStrategyFi
|
|||||||
return wrapOrRoot(branches, pairMeta('ICHIMOKU_TREND', 'sell', level));
|
return wrapOrRoot(branches, pairMeta('ICHIMOKU_TREND', 'sell', level));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 일목 삼역호전 — 매수 트리거 (1회성 돌파) */
|
/** 일목 삼역호전 — N봉 내 존재 윈도우 (3m 기준: 12≈36분, 20≈1h, 26≈78분) */
|
||||||
|
function ichimokuSanyakuWindow(level: StableStrategyFilterLevel): number {
|
||||||
|
switch (level) {
|
||||||
|
case 'relaxed': return 12;
|
||||||
|
case 'balanced': return 20;
|
||||||
|
case 'strict': return 26;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 일목 삼역호전 — 매수 트리거 (구름/후행 돌파, N봉 내 존재) */
|
||||||
function ichimokuSanyakuBuyTrigger(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode {
|
function ichimokuSanyakuBuyTrigger(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode {
|
||||||
|
const w = ichimokuSanyakuWindow(level);
|
||||||
if (level === 'strict') {
|
if (level === 'strict') {
|
||||||
return wrapOr([
|
return wrapOr([
|
||||||
makeCondition('ICHIMOKU', 'CLOUD_BREAK_UP', 'NONE', 'NONE', def),
|
makeExistsIn('ICHIMOKU', 'CLOUD_BREAK_UP', 'NONE', 'NONE', def, w),
|
||||||
makeCondition('ICHIMOKU', 'CHIKOU_CROSS_ABOVE_PRIOR_CLOUD', 'NONE', 'NONE', def),
|
makeExistsIn('ICHIMOKU', 'CHIKOU_CROSS_ABOVE_PRIOR_CLOUD', 'NONE', 'NONE', def, w),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return makeCondition('ICHIMOKU', 'CLOUD_BREAK_UP', 'NONE', 'NONE', def);
|
return makeExistsIn('ICHIMOKU', 'CLOUD_BREAK_UP', 'NONE', 'NONE', def, w);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 구름 돌파 직후에는 전환>기준·양운이 아직 false인 경우가 많음(전형적 바닥 반등).
|
* 구름 돌파 직후에는 전환>기준·양운이 아직 false인 경우가 많음.
|
||||||
* 보통: 둘 중 하나만 충족하면 됨 / 강함: 둘 다 + 최근 N봉 내 양운 형성 허용
|
* N봉 내 존재로 전후 봉에 분산된 추세 확인 허용.
|
||||||
*/
|
*/
|
||||||
function ichimokuSanyakuTrendConfirm(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode[] {
|
function ichimokuSanyakuTrendConfirm(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode[] {
|
||||||
if (level === 'relaxed') return [];
|
if (level === 'relaxed') return [];
|
||||||
if (level === 'balanced') {
|
const w = ichimokuSanyakuWindow(level);
|
||||||
return [
|
|
||||||
wrapOr([
|
|
||||||
makeCondition('ICHIMOKU', 'GT', 'CONVERSION_LINE', 'BASE_LINE', def),
|
|
||||||
makeCondition('ICHIMOKU', 'SPAN1_GT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2', def),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [
|
return [
|
||||||
makeCondition('ICHIMOKU', 'GT', 'CONVERSION_LINE', 'BASE_LINE', def),
|
wrapOr([
|
||||||
makeCondition('ICHIMOKU', 'SPAN1_GT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2', def, {
|
makeExistsIn('ICHIMOKU', 'GT', 'CONVERSION_LINE', 'BASE_LINE', def, w),
|
||||||
candleRangeMode: 'EXISTS_IN',
|
makeExistsIn('ICHIMOKU', 'SPAN1_GT_SPAN2', 'LEADING_SPAN1', 'LEADING_SPAN2', def, w),
|
||||||
candleRange: 26,
|
]),
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 일목 삼역호전 — 공통 추세·구름 필터 (돌파 봉 기준) */
|
/** 일목 삼역호전 — 공통 추세·구름 필터 (N봉 내 존재) */
|
||||||
function ichimokuSanyakuCoreFilters(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode[] {
|
function ichimokuSanyakuCoreFilters(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode[] {
|
||||||
|
const w = ichimokuSanyakuWindow(level);
|
||||||
if (level === 'relaxed') {
|
if (level === 'relaxed') {
|
||||||
return [
|
return [
|
||||||
makeCondition('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'CONVERSION_LINE', def),
|
makeExistsIn('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'CONVERSION_LINE', def, w),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
const nodes: LogicNode[] = [
|
const nodes: LogicNode[] = [
|
||||||
makeCondition('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'CONVERSION_LINE', def),
|
makeExistsIn('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'CONVERSION_LINE', def, w),
|
||||||
makeCondition('ICHIMOKU', 'LAGGING_GT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE', def),
|
makeExistsIn('ICHIMOKU', 'LAGGING_GT_PRICE', 'LAGGING_SPAN', 'CLOSE_PRICE', def, w),
|
||||||
...ichimokuSanyakuTrendConfirm(def, level),
|
...ichimokuSanyakuTrendConfirm(def, level),
|
||||||
];
|
];
|
||||||
if (level === 'strict') {
|
if (level === 'strict') {
|
||||||
nodes.splice(1, 0, makeCondition('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'BASE_LINE', def));
|
nodes.splice(1, 0, makeExistsIn('ICHIMOKU', 'GT', 'CLOSE_PRICE', 'BASE_LINE', def, w));
|
||||||
nodes.push(makeCondition('ICHIMOKU', 'LAGGING_ABOVE_PRIOR_CLOUD', 'NONE', 'NONE', def));
|
nodes.push(makeExistsIn('ICHIMOKU', 'LAGGING_ABOVE_PRIOR_CLOUD', 'NONE', 'NONE', def, w));
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
@@ -431,19 +457,21 @@ function buildIchimokuSanyakuBuy(def: IndicatorPeriodDef, level: StableStrategyF
|
|||||||
...ichimokuSanyakuCoreFilters(def, level),
|
...ichimokuSanyakuCoreFilters(def, level),
|
||||||
];
|
];
|
||||||
if (level === 'strict') {
|
if (level === 'strict') {
|
||||||
nodes.push(makeCondition('MA', 'GT', 'CLOSE_PRICE', maField(60), def));
|
const w = ichimokuSanyakuWindow(level);
|
||||||
nodes.push(makeVolumeMaRatioFilter(def, 1.5, 5));
|
nodes.push(makeExistsIn('MA', 'GT', 'CLOSE_PRICE', maField(60), def, w));
|
||||||
|
nodes.push(makeVolumeMaRatioFilter(def, 1.5, 5, w));
|
||||||
}
|
}
|
||||||
return wrapAnd(nodes, pairMeta('ICHIMOKU_SANYAKU', 'buy', level));
|
return wrapAnd(nodes, pairMeta('ICHIMOKU_SANYAKU', 'buy', level));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 일목 삼역호전 — 청산 (돌파 이벤트 + 이탈 fallback, 포지션 교대 시 1회) */
|
/** 일목 삼역호전 — 청산 (돌파·이탈 N봉 내 존재 + 전환선 이탈 fallback) */
|
||||||
function buildIchimokuSanyakuSell(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode {
|
function buildIchimokuSanyakuSell(def: IndicatorPeriodDef, level: StableStrategyFilterLevel): LogicNode {
|
||||||
|
const w = ichimokuSanyakuWindow(level);
|
||||||
const branches: LogicNode[] = [
|
const branches: LogicNode[] = [
|
||||||
makeCondition('ICHIMOKU', 'CROSS_DOWN', 'CLOSE_PRICE', 'CONVERSION_LINE', def),
|
makeExistsIn('ICHIMOKU', 'CROSS_DOWN', 'CLOSE_PRICE', 'CONVERSION_LINE', def, w),
|
||||||
makeCondition('ICHIMOKU', 'CROSS_DOWN', 'CLOSE_PRICE', 'BASE_LINE', def),
|
makeExistsIn('ICHIMOKU', 'CROSS_DOWN', 'CLOSE_PRICE', 'BASE_LINE', def, w),
|
||||||
makeCondition('ICHIMOKU', 'CROSS_DOWN', 'CONVERSION_LINE', 'BASE_LINE', def),
|
makeExistsIn('ICHIMOKU', 'CROSS_DOWN', 'CONVERSION_LINE', 'BASE_LINE', def, w),
|
||||||
makeCondition('ICHIMOKU', 'CLOUD_BREAK_DOWN', 'CLOSE_PRICE', 'NONE', def),
|
makeExistsIn('ICHIMOKU', 'CLOUD_BREAK_DOWN', 'CLOSE_PRICE', 'NONE', def, w),
|
||||||
];
|
];
|
||||||
if (level !== 'strict') {
|
if (level !== 'strict') {
|
||||||
branches.push(makeCondition('ICHIMOKU', 'LT', 'CLOSE_PRICE', 'CONVERSION_LINE', def));
|
branches.push(makeCondition('ICHIMOKU', 'LT', 'CLOSE_PRICE', 'CONVERSION_LINE', def));
|
||||||
@@ -572,9 +600,10 @@ export function stableStrategyFilterSummary(
|
|||||||
}
|
}
|
||||||
if (strategyId === 'ICHIMOKU_TREND' && level === 'relaxed') parts.push('구름필터 없음');
|
if (strategyId === 'ICHIMOKU_TREND' && level === 'relaxed') parts.push('구름필터 없음');
|
||||||
if (strategyId === 'ICHIMOKU_SANYAKU') {
|
if (strategyId === 'ICHIMOKU_SANYAKU') {
|
||||||
if (level === 'strict') parts.push('삼역 전체·MA60·거래량150%');
|
const w = ichimokuSanyakuWindow(level);
|
||||||
else if (level === 'balanced') parts.push('구름돌파·(전환>기준 OR 양운)');
|
if (level === 'strict') parts.push(`N${w}봉·삼역·MA60·거래량`);
|
||||||
else parts.push('구름돌파·종가>전환');
|
else if (level === 'balanced') parts.push(`N${w}봉·구름돌파·(전환>기준 OR 양운)`);
|
||||||
|
else parts.push(`N${w}봉·구름돌파·종가>전환`);
|
||||||
}
|
}
|
||||||
if (strategyId === 'MACD_MOMENTUM' && level === 'relaxed') parts.push('EMA60·ADX 없음');
|
if (strategyId === 'MACD_MOMENTUM' && level === 'relaxed') parts.push('EMA60·ADX 없음');
|
||||||
if (strategyId === 'MA_TREND' && level === 'relaxed') parts.push('ADX 없음');
|
if (strategyId === 'MA_TREND' && level === 'relaxed') parts.push('ADX 없음');
|
||||||
@@ -607,6 +636,7 @@ export function inferStableStrategyFilterLevel(node: LogicNode): StableStrategyF
|
|||||||
return 'relaxed';
|
return 'relaxed';
|
||||||
}
|
}
|
||||||
if (id === 'ICHIMOKU_SANYAKU') {
|
if (id === 'ICHIMOKU_SANYAKU') {
|
||||||
|
const hasWindow = conds.some(c => c.candleRangeMode === 'EXISTS_IN');
|
||||||
const hasCloudBreak = conds.some(c => c.conditionType === 'CLOUD_BREAK_UP');
|
const hasCloudBreak = conds.some(c => c.conditionType === 'CLOUD_BREAK_UP');
|
||||||
const hasChikouCross = conds.some(c => c.conditionType === 'CHIKOU_CROSS_ABOVE_PRIOR_CLOUD');
|
const hasChikouCross = conds.some(c => c.conditionType === 'CHIKOU_CROSS_ABOVE_PRIOR_CLOUD');
|
||||||
const hasLaggingCloud = conds.some(c => c.conditionType === 'LAGGING_ABOVE_PRIOR_CLOUD');
|
const hasLaggingCloud = conds.some(c => c.conditionType === 'LAGGING_ABOVE_PRIOR_CLOUD');
|
||||||
@@ -615,6 +645,7 @@ export function inferStableStrategyFilterLevel(node: LogicNode): StableStrategyF
|
|||||||
const hasLagging = conds.some(c => c.conditionType === 'LAGGING_GT_PRICE');
|
const hasLagging = conds.some(c => c.conditionType === 'LAGGING_GT_PRICE');
|
||||||
if (hasVolRatio || hasLaggingCloud) return 'strict';
|
if (hasVolRatio || hasLaggingCloud) return 'strict';
|
||||||
if (hasMa) return 'strict';
|
if (hasMa) return 'strict';
|
||||||
|
if (hasLagging && (hasCloudBreak || hasChikouCross) && hasWindow) return 'balanced';
|
||||||
if (hasLagging && (hasCloudBreak || hasChikouCross)) return 'balanced';
|
if (hasLagging && (hasCloudBreak || hasChikouCross)) return 'balanced';
|
||||||
if (hasCloudBreak || hasChikouCross) return 'relaxed';
|
if (hasCloudBreak || hasChikouCross) return 'relaxed';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user