Files
goldenChart/frontend/src/utils/stochOverboughtPair.ts
T
2026-06-11 22:53:19 +09:00

195 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 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),
};
}