복합지표 추가
This commit is contained in:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user