195 lines
6.0 KiB
TypeScript
195 lines
6.0 KiB
TypeScript
/** 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),
|
||
};
|
||
}
|