복합지표 추가

This commit is contained in:
Macbook
2026-06-11 22:53:19 +09:00
parent 280c187021
commit 05c15ec92b
17 changed files with 817 additions and 77 deletions
+194
View File
@@ -0,0 +1,194 @@
/** Stochastic 과열(≥80) × 보조지표 중앙선 — 순차 OR 복합 매수 조건 */
import type { ConditionDSL, LogicNode } from './strategyTypes';
import { initConditionPeriodsInherit, type IndicatorPeriodDef } from './conditionPeriods';
import { genId } from './strategyNodeIds';
import { THRESHOLD_HL_MID, THRESHOLD_HL_OVER } from './thresholdSymbols';
export const STOCH_OVERBOUGHT_PAIR_VALUE = 'STOCH_OVERBOUGHT_PAIR';
export const STOCH_PAIR_SECONDARY_OPTIONS: { value: string; label: string }[] = [
{ value: 'CCI', label: 'CCI' },
{ value: 'RSI', label: 'RSI' },
{ value: 'ADX', label: 'ADX' },
{ value: 'WILLIAMS_R', label: 'Williams %R' },
{ value: 'TRIX', label: 'TRIX' },
{ value: 'VR', label: 'VR' },
{ value: 'PSYCHOLOGICAL', label: '심리도' },
{ value: 'NEW_PSYCHOLOGICAL', label: '신심리도' },
{ value: 'INVEST_PSYCHOLOGICAL', label: '투자심리도' },
{ value: 'BWI', label: 'BWI' },
{ value: 'VOLUME_OSC', label: 'Volume OSC' },
];
const SECONDARY_VALUE_FIELD: Record<string, string> = {
RSI: 'RSI_VALUE',
CCI: 'CCI_VALUE',
ADX: 'ADX_VALUE',
WILLIAMS_R: 'WILLIAMS_R_VALUE',
TRIX: 'TRIX_VALUE',
VR: 'VR_VALUE',
PSYCHOLOGICAL: 'PSY_VALUE',
NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE',
INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE',
BWI: 'BWI_VALUE',
VOLUME_OSC: 'VOLUME_OSC_VALUE',
};
function secondaryLabel(indicatorType: string): string {
return STOCH_PAIR_SECONDARY_OPTIONS.find(o => o.value === indicatorType)?.label ?? indicatorType;
}
export function isStochOverboughtPairPaletteValue(value: string): boolean {
return value === STOCH_OVERBOUGHT_PAIR_VALUE;
}
export function isStochOverboughtPairRoot(node: LogicNode): boolean {
return node.type === 'OR' && !!node.stochPair?.secondaryIndicatorType;
}
export function getSecondaryValueField(indicatorType: string): string {
return SECONDARY_VALUE_FIELD[indicatorType] ?? `${indicatorType}_VALUE`;
}
function makePairCondition(
indicatorType: string,
conditionType: string,
leftField: string,
rightField: string,
def: IndicatorPeriodDef,
): LogicNode {
const base: ConditionDSL = {
indicatorType,
conditionType,
leftField,
rightField,
candleRange: 1,
valuePeriodOverride: false,
thresholdOverride: false,
rightPeriodOverride: false,
};
return {
id: genId(),
type: 'CONDITION',
condition: initConditionPeriodsInherit(indicatorType, base, def),
};
}
/** (Stoch≥과열 AND 보조 CROSS_UP 중앙) OR (보조≥중앙 AND Stoch CROSS_UP 과열) */
export function buildStochOverboughtPairTree(
secondaryIndicator: string,
def: IndicatorPeriodDef,
): LogicNode {
const sec = STOCH_PAIR_SECONDARY_OPTIONS.some(o => o.value === secondaryIndicator)
? secondaryIndicator
: 'CCI';
const secField = getSecondaryValueField(sec);
return {
id: genId(),
type: 'OR',
stochPair: { secondaryIndicatorType: sec },
children: [
{
id: genId(),
type: 'AND',
children: [
makePairCondition('STOCHASTIC', 'GTE', 'STOCH_K', THRESHOLD_HL_OVER, def),
makePairCondition(sec, 'CROSS_UP', secField, THRESHOLD_HL_MID, def),
],
},
{
id: genId(),
type: 'AND',
children: [
makePairCondition(sec, 'GTE', secField, THRESHOLD_HL_MID, def),
makePairCondition('STOCHASTIC', 'CROSS_UP', 'STOCH_K', THRESHOLD_HL_OVER, def),
],
},
],
};
}
/** 보조지표 변경 시 기존 OR·AND 노드 id 유지 */
export function setStochPairSecondary(
root: LogicNode,
secondaryIndicator: string,
def: IndicatorPeriodDef,
): LogicNode {
if (!isStochOverboughtPairRoot(root)) return root;
const rebuilt = buildStochOverboughtPairTree(secondaryIndicator, def);
const preserveIds = (next: LogicNode, prev: LogicNode | undefined): LogicNode => ({
...next,
id: prev?.id ?? next.id,
children: next.children?.map((child, i) =>
preserveIds(child, prev?.children?.[i]),
),
});
return preserveIds(rebuilt, root);
}
export function stochPairDisplayName(secondaryIndicator: string): string {
return `Stoch 과열×${secondaryLabel(secondaryIndicator)}`;
}
export function stochPairSummaryText(secondaryIndicator: string): string {
const sec = secondaryLabel(secondaryIndicator);
return `(Stoch≥과열 AND ${sec} 중앙 상향) OR (${sec}≥중앙 AND Stoch 과열 상향)`;
}
export function stochPairPaletteDesc(secondaryIndicator: string): string {
const sec = secondaryLabel(secondaryIndicator);
return `Stoch≥80 + ${sec} 0선(중앙) 돌파 또는 ${sec}≥중앙 + Stoch 과열 돌파`;
}
function nodeContainsId(node: LogicNode, targetId: string): boolean {
if (node.id === targetId) return true;
return (node.children ?? []).some(c => nodeContainsId(c, targetId));
}
/** nodeId를 포함하는 Stoch 과열×보조 OR 루트 탐색 */
export function findStochPairRootContaining(
node: LogicNode | null | undefined,
targetId: string,
): LogicNode | null {
if (!node) return null;
if (isStochOverboughtPairRoot(node) && nodeContainsId(node, targetId)) {
return node;
}
for (const child of node.children ?? []) {
const hit = findStochPairRootContaining(child, targetId);
if (hit) return hit;
}
return null;
}
export function findStochPairRootInForest(
roots: (LogicNode | null | undefined)[],
targetId: string,
): LogicNode | null {
for (const root of roots) {
const hit = findStochPairRootContaining(root, targetId);
if (hit) return hit;
}
return null;
}
/** OR 서브트리 전체를 새 보조지표로 재구성 (노드 id 유지) */
export function patchStochPairInTree(
root: LogicNode | null,
orNodeId: string,
secondaryIndicator: string,
def: IndicatorPeriodDef,
): LogicNode | null {
if (!root) return null;
if (root.id === orNodeId && isStochOverboughtPairRoot(root)) {
return setStochPairSecondary(root, secondaryIndicator, def);
}
if (!root.children?.length) return root;
return {
...root,
children: root.children.map(c => patchStochPairInTree(c, orNodeId, secondaryIndicator, def) ?? c),
};
}