복합지표 추가

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
+84 -22
View File
@@ -4,6 +4,7 @@ import type { IndicatorConfig } from '../types/index';
import { getIndicatorDef, getHLineLabel, INDICATOR_REGISTRY, type PlotDef, type HLineDef } from '../utils/indicatorRegistry';
import type { LogicNode, LogicNodeType, ConditionDSL } from '../utils/strategyTypes';
import {
COMPOSITE_INDICATOR_ITEMS,
compositeDisplayName,
compositeElementLabel,
compositeFieldLabel,
@@ -11,6 +12,7 @@ import {
normalizeCompositeCondition,
parseDonchianPeriod,
parsePeriodFromCompositeField,
switchCompositeIndicatorType,
syncCompositeFields,
} from '../utils/compositeIndicators';
import {
@@ -21,6 +23,14 @@ import {
syncPriceExtremeSimpleFields,
} from '../utils/priceExtremeIndicators';
import { getStrategyIndicatorDisplayName } from '../utils/strategyPaletteStorage';
import {
buildStochOverboughtPairTree,
isStochOverboughtPairPaletteValue,
isStochOverboughtPairRoot,
STOCH_PAIR_SECONDARY_OPTIONS,
stochPairDisplayName,
stochPairSummaryText,
} from '../utils/stochOverboughtPair';
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
import {
getCompositeFieldOpts,
@@ -57,6 +67,9 @@ import {
import { STRATEGY_CANDLE_TYPE_OPTIONS } from '../utils/strategyStartNodes';
import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
import { genId } from './strategyNodeIds';
export { genId };
export interface StrategyDto {
id: number;
@@ -78,9 +91,6 @@ export interface ValidationResult {
export type DefType = typeof DEF_DEFAULTS;
let _cnt = 0;
export const genId = () => `n_${++_cnt}_${Date.now()}`;
/** @deprecated DB(gc_strategy)만 사용 */
export const loadStratsLocal = (): StrategyDto[] => [];
/** @deprecated */
@@ -729,6 +739,10 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
return parts.length === 0 ? '(AND 그룹)' : parts.join('\nAND ');
}
if (node.type === 'OR') {
if (isStochOverboughtPairRoot(node)) {
const sec = node.stochPair!.secondaryIndicatorType;
return `${stochPairDisplayName(sec)}\n${stochPairSummaryText(sec)}`;
}
const parts = (node.children ?? []).map(c => nodeToText(c, DEF));
return parts.length === 0 ? '(OR 그룹)' : parts.join('\nOR ');
}
@@ -818,9 +832,17 @@ interface CondEditorProps {
signalType: 'buy'|'sell';
onChange: (c: ConditionDSL) => void;
def: DefType;
/** Stoch 과열×보조 복합 — 캔들범위 대신 보조지표 선택 */
stochPairEdit?: {
secondaryIndicator: string;
onSecondaryChange: (indicator: string) => void;
stochLocked?: boolean;
};
}
export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChange, def }) => {
export const CondEditor: React.FC<CondEditorProps> = ({
cond, signalType, onChange, def, stochPairEdit,
}) => {
const normalized = normalizeCompositeCondition(cond);
const fieldOpts = getFieldOpts(normalized.indicatorType, signalType, def, normalized);
const condOpts = normalized.indicatorType === 'ICHIMOKU' ? ICHIMOKU_CONDS : COND_OPTIONS;
@@ -936,14 +958,15 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
</div>
<div className="sp-cond-row4">
<div className="sp-cond-field">
<label className="sp-cond-lbl"> </label>
<select className="sp-cond-sel" value={normalized.candleRange ?? 1}
onChange={e => onChange({ ...normalized, candleRange: Number(e.target.value) })}>
<option value={1}> </option>
<option value={2}>2 </option>
<option value={3}>3 </option>
<option value={4}>4 </option>
<option value={5}>5 </option>
<label className="sp-cond-lbl"></label>
<select
className="sp-cond-sel"
value={normalized.indicatorType}
onChange={e => onChange(switchCompositeIndicatorType(normalized, e.target.value, def))}
>
{COMPOSITE_INDICATOR_ITEMS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
<div className="sp-cond-field">
@@ -995,17 +1018,41 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
<div className="sp-cond-editor">
{/* 4컬럼 row */}
<div className="sp-cond-row4">
{/* 캔들 범위 */}
{/* 캔들 범위 / Stoch×보조 복합 보조지표 */}
<div className="sp-cond-field">
<label className="sp-cond-lbl"> </label>
<select className="sp-cond-sel" value={cond.candleRange ?? 1}
onChange={e => onChange({ ...cond, candleRange: Number(e.target.value) })}>
<option value={1}> </option>
<option value={2}>2 </option>
<option value={3}>3 </option>
<option value={4}>4 </option>
<option value={5}>5 </option>
</select>
{stochPairEdit ? (
stochPairEdit.stochLocked ? (
<>
<label className="sp-cond-lbl"> </label>
<output className="sp-cond-readout">Stochastic %K</output>
</>
) : (
<>
<label className="sp-cond-lbl"></label>
<select
className="sp-cond-sel"
value={stochPairEdit.secondaryIndicator}
onChange={e => stochPairEdit.onSecondaryChange(e.target.value)}
>
{STOCH_PAIR_SECONDARY_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</>
)
) : (
<>
<label className="sp-cond-lbl"> </label>
<select className="sp-cond-sel" value={cond.candleRange ?? 1}
onChange={e => onChange({ ...cond, candleRange: Number(e.target.value) })}>
<option value={1}> </option>
<option value={2}>2 </option>
<option value={3}>3 </option>
<option value={4}>4 </option>
<option value={5}>5 </option>
</select>
</>
)}
</div>
{/* 조건대상1 */}
<div className="sp-cond-field">
@@ -1117,17 +1164,29 @@ export type MakeNodeOptions = {
rightPeriod?: number;
leftCandleType?: string;
rightCandleType?: string;
/** Stoch 과열×보조 복합 */
stochPair?: boolean;
secondaryIndicator?: string;
};
/** 팔레트 드래그 payload → makeNode 옵션 */
export function makeNodeOptionsFromPalette(data: {
composite?: boolean;
stochPair?: boolean;
secondaryIndicator?: string;
period?: number;
shortPeriod?: number;
longPeriod?: number;
leftCandleType?: string;
rightCandleType?: string;
value?: string;
}): MakeNodeOptions | undefined {
if (data.stochPair || (data.value && isStochOverboughtPairPaletteValue(data.value))) {
return {
stochPair: true,
secondaryIndicator: data.secondaryIndicator ?? 'CCI',
};
}
if (data.composite) {
return {
composite: true,
@@ -1146,6 +1205,9 @@ export const makeNode = (
options?: MakeNodeOptions,
): LogicNode => {
if (type === 'operator') return { id: genId(), type: value as LogicNodeType, children: [] };
if (options?.stochPair || isStochOverboughtPairPaletteValue(value)) {
return buildStochOverboughtPairTree(options?.secondaryIndicator ?? 'CCI', DEF);
}
if (options?.composite) {
let condition = makeCompositeCondition(value, signalType, DEF);
condition = {