지표탭 추가
This commit is contained in:
@@ -31,7 +31,7 @@ export interface IndicatorPeriodDef {
|
||||
investPsy: number;
|
||||
}
|
||||
|
||||
const VALUE_FIELD_PREFIX: Record<string, string> = {
|
||||
export const VALUE_FIELD_PREFIX: Record<string, string> = {
|
||||
RSI: 'RSI_VALUE',
|
||||
CCI: 'CCI_VALUE',
|
||||
ADX: 'ADX_VALUE',
|
||||
@@ -69,33 +69,70 @@ function parseFieldPeriod(field?: string): number | null {
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
/** 조건 노드의 RSI 등 값(좌측) 기간 — 오버라이드 시 DSL, 아니면 보조지표 설정(DEF) */
|
||||
export function isValuePeriodOverridden(cond: ConditionDSL): boolean {
|
||||
if (cond.valuePeriodOverride === true) return true;
|
||||
if (cond.valuePeriodOverride === false) return false;
|
||||
if (cond.period != null && cond.period > 0) return true;
|
||||
if (cond.leftPeriod != null && cond.leftPeriod > 0) return true;
|
||||
if (isValuePeriodFieldStored(cond.indicatorType, cond.leftField)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isRightPeriodOverridden(cond: ConditionDSL): boolean {
|
||||
if (cond.rightPeriodOverride === true) return true;
|
||||
if (cond.rightPeriodOverride === false) return false;
|
||||
if (cond.rightPeriod != null && cond.rightPeriod > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isThresholdOverridden(cond: ConditionDSL): boolean {
|
||||
if (cond.thresholdOverride === true) return true;
|
||||
if (cond.thresholdOverride === false) return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 조건 노드의 RSI 등 값(좌측) 기간 */
|
||||
export function getConditionValuePeriod(cond: ConditionDSL, def: IndicatorPeriodDef): number {
|
||||
if (isPriceExtremeIndicator(cond.indicatorType)) {
|
||||
if (cond.period && cond.period > 0) return cond.period;
|
||||
const fromRight = parsePriorExtremePeriod(cond.rightField);
|
||||
if (fromRight) return fromRight;
|
||||
if (isValuePeriodOverridden(cond)) {
|
||||
if (cond.period && cond.period > 0) return cond.period;
|
||||
const fromRight = parsePriorExtremePeriod(cond.rightField);
|
||||
if (fromRight) return fromRight;
|
||||
}
|
||||
return getDefaultIndicatorPeriod(cond.indicatorType, def);
|
||||
}
|
||||
if (cond.composite) {
|
||||
if (cond.leftPeriod && cond.leftPeriod > 0) return cond.leftPeriod;
|
||||
const fromField = parsePeriodFromCompositeField(cond.indicatorType, cond.leftField);
|
||||
if (isValuePeriodOverridden(cond)) {
|
||||
if (cond.leftPeriod && cond.leftPeriod > 0) return cond.leftPeriod;
|
||||
const fromField = parsePeriodFromCompositeField(cond.indicatorType, cond.leftField);
|
||||
if (fromField) return fromField;
|
||||
}
|
||||
const { short } = getCompositeDefaultPeriods(cond.indicatorType, def as CompositePeriodDef);
|
||||
return short;
|
||||
}
|
||||
if (isValuePeriodOverridden(cond)) {
|
||||
if (cond.period && cond.period > 0) return cond.period;
|
||||
const fromField = parseFieldPeriod(cond.leftField);
|
||||
if (fromField) return fromField;
|
||||
}
|
||||
if (cond.period && cond.period > 0) return cond.period;
|
||||
const fromField = parseFieldPeriod(cond.leftField);
|
||||
if (fromField) return fromField;
|
||||
return getDefaultIndicatorPeriod(cond.indicatorType, def);
|
||||
}
|
||||
|
||||
export function getConditionRightPeriod(cond: ConditionDSL, def: IndicatorPeriodDef): number {
|
||||
if (cond.composite) {
|
||||
if (cond.rightPeriod && cond.rightPeriod > 0) return cond.rightPeriod;
|
||||
const fromField = parsePeriodFromCompositeField(cond.indicatorType, cond.rightField);
|
||||
if (isRightPeriodOverridden(cond)) {
|
||||
if (cond.rightPeriod && cond.rightPeriod > 0) return cond.rightPeriod;
|
||||
const fromField = parsePeriodFromCompositeField(cond.indicatorType, cond.rightField);
|
||||
if (fromField) return fromField;
|
||||
}
|
||||
const { long } = getCompositeDefaultPeriods(cond.indicatorType, def as CompositePeriodDef);
|
||||
return long;
|
||||
}
|
||||
if (isValuePeriodOverridden(cond)) {
|
||||
const fromField = parseFieldPeriod(cond.rightField);
|
||||
if (fromField) return fromField;
|
||||
}
|
||||
const fromField = parseFieldPeriod(cond.rightField);
|
||||
if (fromField) return fromField;
|
||||
return getDefaultIndicatorPeriod(cond.indicatorType, def);
|
||||
}
|
||||
|
||||
@@ -142,6 +179,9 @@ export function hasEditableThreshold(cond: ConditionDSL): boolean {
|
||||
}
|
||||
|
||||
export function getConditionThreshold(cond: ConditionDSL, side: 'left' | 'right'): number | null {
|
||||
if (side === 'right' && !isThresholdOverridden(cond)) {
|
||||
return parseThresholdField(cond.rightField);
|
||||
}
|
||||
const field = side === 'left' ? cond.leftField : cond.rightField;
|
||||
return parseThresholdField(field);
|
||||
}
|
||||
@@ -153,13 +193,13 @@ export function setConditionValuePeriod(
|
||||
): ConditionDSL {
|
||||
const p = Math.max(1, Math.min(500, Math.round(period)));
|
||||
if (isPriceExtremeIndicator(cond.indicatorType)) {
|
||||
return syncPriceExtremeSimpleFields({ ...cond, period: p });
|
||||
return syncPriceExtremeSimpleFields({ ...cond, period: p, valuePeriodOverride: true });
|
||||
}
|
||||
if (cond.composite) {
|
||||
return syncCompositeFields({ ...cond, composite: true, leftPeriod: p });
|
||||
return syncCompositeFields({ ...cond, composite: true, leftPeriod: p, valuePeriodOverride: true });
|
||||
}
|
||||
const prefix = VALUE_FIELD_PREFIX[cond.indicatorType];
|
||||
const next: ConditionDSL = { ...cond, period: p };
|
||||
const next: ConditionDSL = { ...cond, period: p, valuePeriodOverride: true };
|
||||
if (prefix && (cond.leftField === prefix || cond.leftField?.startsWith(`${prefix}_`))) {
|
||||
next.leftField = `${prefix}_${p}`;
|
||||
}
|
||||
@@ -169,7 +209,7 @@ export function setConditionValuePeriod(
|
||||
export function setConditionRightPeriod(cond: ConditionDSL, period: number): ConditionDSL {
|
||||
const p = Math.max(1, Math.min(500, Math.round(period)));
|
||||
if (!cond.composite) return cond;
|
||||
return syncCompositeFields({ ...cond, composite: true, rightPeriod: p });
|
||||
return syncCompositeFields({ ...cond, composite: true, rightPeriod: p, rightPeriodOverride: true });
|
||||
}
|
||||
|
||||
export function setConditionThreshold(
|
||||
@@ -184,9 +224,41 @@ export function setConditionThreshold(
|
||||
...cond,
|
||||
[fieldKey]: thresholdField(value),
|
||||
targetValue: value,
|
||||
thresholdOverride: true,
|
||||
};
|
||||
}
|
||||
|
||||
/** 신규 조건 노드 — 보조지표 설정 기본값 상속(오버라이드 없음) */
|
||||
export function initConditionPeriodsInherit(
|
||||
indicatorType: string,
|
||||
condition: ConditionDSL,
|
||||
def: IndicatorPeriodDef,
|
||||
): ConditionDSL {
|
||||
const base = {
|
||||
...condition,
|
||||
valuePeriodOverride: false as const,
|
||||
thresholdOverride: false as const,
|
||||
rightPeriodOverride: false as const,
|
||||
};
|
||||
if (isPriceExtremeIndicator(indicatorType)) {
|
||||
const period = getDefaultIndicatorPeriod(indicatorType, def);
|
||||
return syncPriceExtremeSimpleFields({ ...base, period });
|
||||
}
|
||||
if (condition.composite) return base;
|
||||
const prefix = VALUE_FIELD_PREFIX[indicatorType];
|
||||
if (!prefix) {
|
||||
const { period: _p, ...rest } = base;
|
||||
return rest;
|
||||
}
|
||||
const lf = condition.leftField ?? '';
|
||||
if (lf === prefix || lf.startsWith(`${prefix}_`)) {
|
||||
const { period: _p, ...rest } = base;
|
||||
return { ...rest, leftField: prefix };
|
||||
}
|
||||
const { period: _p, ...rest } = base;
|
||||
return rest;
|
||||
}
|
||||
|
||||
export function initConditionPeriods(
|
||||
indicatorType: string,
|
||||
condition: ConditionDSL,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { IndicatorConfig } from '../types';
|
||||
import { enrichIndicatorConfig, getIndicatorDef } from './indicatorRegistry';
|
||||
import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from './smaConfig';
|
||||
import { normalizeIchimokuConfig } from './ichimokuConfig';
|
||||
import type { PlotDef, HLineDef } from './indicatorRegistry';
|
||||
import type { IchimokuCloudColors } from './ichimokuConfig';
|
||||
|
||||
/** 보조지표 인스턴스 복제 (새 id, 별도 pane) */
|
||||
export function cloneIndicatorConfig(src: IndicatorConfig, newId: string): IndicatorConfig {
|
||||
@@ -137,3 +141,54 @@ export function splitIndicatorPane(
|
||||
export function layoutKey(inds: IndicatorConfig[]): string {
|
||||
return inds.map(i => `${i.id}>${i.mergedWith ?? ''}`).join(';');
|
||||
}
|
||||
|
||||
type GetParams = (
|
||||
type: string,
|
||||
defaults?: Record<string, number | string | boolean>,
|
||||
) => Record<string, number | string | boolean>;
|
||||
|
||||
type GetVisual = (
|
||||
type: string,
|
||||
defaultPlots?: PlotDef[],
|
||||
defaultHlines?: HLineDef[],
|
||||
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors };
|
||||
|
||||
/** 지표 타입 목록 → 차트에 추가할 IndicatorConfig 배열 (이미 활성인 type 제외) */
|
||||
export function buildIndicatorConfigsFromTypes(
|
||||
types: string[],
|
||||
existing: IndicatorConfig[],
|
||||
getParams: GetParams,
|
||||
getVisualConfig: GetVisual,
|
||||
newId: () => string,
|
||||
): IndicatorConfig[] {
|
||||
const seen = new Set(existing.map(i => i.type));
|
||||
const out: IndicatorConfig[] = [];
|
||||
|
||||
for (const type of types) {
|
||||
if (seen.has(type)) continue;
|
||||
const def = getIndicatorDef(type);
|
||||
if (!def) continue;
|
||||
seen.add(type);
|
||||
|
||||
const params = getParams(type, def.defaultParams);
|
||||
const { plots, hlines, cloudColors } = getVisualConfig(type, def.plots, def.hlines);
|
||||
let cfg: IndicatorConfig = {
|
||||
id: newId(),
|
||||
type: def.type,
|
||||
params,
|
||||
plots,
|
||||
hlines,
|
||||
...(cloudColors ? { cloudColors } : {}),
|
||||
};
|
||||
if (type === 'SMA') {
|
||||
cfg = normalizeSmaConfig({
|
||||
...cfg,
|
||||
plotVisibility: cfg.plotVisibility ?? createDefaultSmaPlotVisibility(),
|
||||
});
|
||||
} else if (type === 'IchimokuCloud') {
|
||||
cfg = normalizeIchimokuConfig(cfg);
|
||||
}
|
||||
out.push(cfg);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/** 보조지표 전역 설정(DEF) 변경 시 전략 조건 트리 동기화 — 오버라이드되지 않은 노드만 */
|
||||
import type { LogicNode } from './strategyTypes';
|
||||
import type { ConditionDSL } from './strategyTypes';
|
||||
import {
|
||||
getCompositeDefaultPeriods,
|
||||
syncCompositeFields,
|
||||
type CompositePeriodDef,
|
||||
} from './compositeIndicators';
|
||||
import {
|
||||
isPriceExtremeIndicator,
|
||||
syncPriceExtremeSimpleFields,
|
||||
} from './priceExtremeIndicators';
|
||||
import {
|
||||
getDefaultIndicatorPeriod,
|
||||
isRightPeriodOverridden,
|
||||
isThresholdOverridden,
|
||||
isValuePeriodOverridden,
|
||||
parseThresholdField,
|
||||
usesValuePeriodField,
|
||||
VALUE_FIELD_PREFIX,
|
||||
} from './conditionPeriods';
|
||||
import type { DefType } from './strategyEditorShared';
|
||||
import { getDefaultConditionFields } from './strategyEditorShared';
|
||||
|
||||
function applyGlobalDefToCondition(
|
||||
cond: ConditionDSL,
|
||||
def: DefType,
|
||||
signalType: 'buy' | 'sell',
|
||||
): ConditionDSL {
|
||||
let next: ConditionDSL = { ...cond };
|
||||
|
||||
if (isPriceExtremeIndicator(cond.indicatorType)) {
|
||||
if (!isValuePeriodOverridden(cond)) {
|
||||
const period = getDefaultIndicatorPeriod(cond.indicatorType, def);
|
||||
next = syncPriceExtremeSimpleFields({ ...next, period, valuePeriodOverride: false });
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
if (cond.composite) {
|
||||
const { short, long } = getCompositeDefaultPeriods(cond.indicatorType, def as CompositePeriodDef);
|
||||
if (!isValuePeriodOverridden(cond)) {
|
||||
next = syncCompositeFields({
|
||||
...next,
|
||||
composite: true,
|
||||
leftPeriod: short,
|
||||
valuePeriodOverride: false,
|
||||
});
|
||||
}
|
||||
if (!isRightPeriodOverridden(cond)) {
|
||||
next = syncCompositeFields({
|
||||
...next,
|
||||
composite: true,
|
||||
rightPeriod: long,
|
||||
rightPeriodOverride: false,
|
||||
});
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
if (!isValuePeriodOverridden(cond)) {
|
||||
const prefix = VALUE_FIELD_PREFIX[cond.indicatorType];
|
||||
const { period: _omit, ...rest } = next;
|
||||
next = { ...rest, valuePeriodOverride: false };
|
||||
if (prefix && usesValuePeriodField(cond)) {
|
||||
next.leftField = prefix;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isThresholdOverridden(cond) && parseThresholdField(next.rightField) != null) {
|
||||
const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def);
|
||||
next.rightField = defaults.r;
|
||||
const parsed = parseThresholdField(defaults.r);
|
||||
if (parsed != null) next.targetValue = parsed;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function syncConditionNode(node: LogicNode, def: DefType, signalType: 'buy' | 'sell'): LogicNode {
|
||||
if (node.type !== 'CONDITION' || !node.condition) return node;
|
||||
return {
|
||||
...node,
|
||||
condition: applyGlobalDefToCondition(node.condition, def, signalType),
|
||||
};
|
||||
}
|
||||
|
||||
function syncTreeNode(node: LogicNode, def: DefType, signalType: 'buy' | 'sell'): LogicNode {
|
||||
let next = syncConditionNode(node, def, signalType);
|
||||
if (next.children?.length) {
|
||||
next = {
|
||||
...next,
|
||||
children: next.children.map(c => syncTreeNode(c, def, signalType)),
|
||||
};
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function syncLogicNodeWithGlobalDef(
|
||||
node: LogicNode,
|
||||
def: DefType,
|
||||
signalType: 'buy' | 'sell',
|
||||
): LogicNode {
|
||||
return syncTreeNode(node, def, signalType);
|
||||
}
|
||||
|
||||
export function syncLogicRootWithGlobalDef(
|
||||
root: LogicNode | null,
|
||||
orphans: LogicNode[],
|
||||
def: DefType,
|
||||
signalType: 'buy' | 'sell',
|
||||
): { root: LogicNode | null; orphans: LogicNode[] } {
|
||||
return {
|
||||
root: root ? syncTreeNode(root, def, signalType) : null,
|
||||
orphans: orphans.map(n => syncTreeNode(n, def, signalType)),
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Shared strategy editor utilities — extracted from StrategyPage */
|
||||
import React from 'react';
|
||||
import type { IndicatorConfig } from '../types/index';
|
||||
import { getIndicatorDef, getHLineLabel } from '../utils/indicatorRegistry';
|
||||
import { getIndicatorDef, getHLineLabel, type PlotDef, type HLineDef } from '../utils/indicatorRegistry';
|
||||
import type { LogicNode, LogicNodeType, ConditionDSL } from '../utils/strategyTypes';
|
||||
import {
|
||||
compositeDisplayName,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
getPeriodPresetOptions,
|
||||
getThresholdBounds,
|
||||
getThresholdPresetOptions,
|
||||
initConditionPeriods,
|
||||
initConditionPeriodsInherit,
|
||||
isValuePeriodFieldStored,
|
||||
parseThresholdField,
|
||||
resolveFieldCustomKind,
|
||||
@@ -256,9 +256,45 @@ export function buildDef(activeIndicators: IndicatorConfig[]): DefType {
|
||||
}
|
||||
|
||||
/**
|
||||
* 전략편집기 전용 DEF — 차트 activeIndicators와 무관한 고정 기본값.
|
||||
* 조건 노드의 기간·임계값은 ConditionDSL(leftPeriod/rightPeriod/period)에만 저장된다.
|
||||
* 전략편집기 DEF — DB 보조지표 설정(파라미터·hline)에서 구성.
|
||||
* 조건 노드 기본값은 valuePeriodOverride/thresholdOverride=false 일 때 여기서 상속.
|
||||
*/
|
||||
export function buildStrategyEditorDefFromSettings(
|
||||
getParams: (
|
||||
type: string,
|
||||
defaults?: Record<string, number | string | boolean>,
|
||||
) => Record<string, number | string | boolean>,
|
||||
getVisualConfig: (
|
||||
type: string,
|
||||
defaultPlots?: PlotDef[],
|
||||
defaultHlines?: HLineDef[],
|
||||
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: unknown },
|
||||
): DefType {
|
||||
const registryTypes = [
|
||||
'RSI', 'MACD', 'CCI', 'Stochastic', 'ADX', 'TRIX', 'DMI', 'BollingerBands',
|
||||
'IchimokuCloud', 'WilliamsPercentRange', 'VolumeOscillator', 'VR', 'Disparity',
|
||||
'Psychological', 'InvestPsychological', 'OBV', 'DonchianChannels', 'PriceExtreme',
|
||||
'SMA',
|
||||
];
|
||||
const configs: IndicatorConfig[] = [];
|
||||
for (const type of registryTypes) {
|
||||
const def = getIndicatorDef(type);
|
||||
if (!def) continue;
|
||||
const params = getParams(type, def.defaultParams as Record<string, number | string | boolean>);
|
||||
const { plots, hlines, cloudColors } = getVisualConfig(type, def.plots, def.hlines);
|
||||
configs.push({
|
||||
id: `se-def-${type}`,
|
||||
type,
|
||||
params: { ...params },
|
||||
plots,
|
||||
hlines,
|
||||
...(cloudColors ? { cloudColors } : {}),
|
||||
} as IndicatorConfig);
|
||||
}
|
||||
return buildDef(configs);
|
||||
}
|
||||
|
||||
/** @deprecated buildStrategyEditorDefFromSettings 사용 — 하드코딩 폴백 */
|
||||
export function buildStrategyEditorDef(): DefType {
|
||||
return buildDef([]);
|
||||
}
|
||||
@@ -529,7 +565,7 @@ export const getFieldOpts = (
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
|
||||
const getDefaultConditionFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
|
||||
// 저장된 hline 과열선 값 우선 사용
|
||||
const over = (def: number) => kv(DEF.hlThresh[ind]?.over ?? def);
|
||||
const mid = (def: number) => kv(DEF.hlThresh[ind]?.mid ?? def);
|
||||
@@ -566,6 +602,8 @@ const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType):
|
||||
return map[ind] ?? { l:'NONE', r:'NONE' };
|
||||
};
|
||||
|
||||
export { getDefaultConditionFields };
|
||||
|
||||
// conditionType 변경 시 자동 필드 조정
|
||||
const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: DefType): ConditionDSL => {
|
||||
const updated = { ...cond, conditionType: newCondType };
|
||||
@@ -579,7 +617,7 @@ const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: Def
|
||||
}
|
||||
const fieldOpts = getFieldOpts(cond.indicatorType, 'buy', DEF);
|
||||
const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
|
||||
const def = getDefaultFields(cond.indicatorType, 'buy', DEF);
|
||||
const def = getDefaultConditionFields(cond.indicatorType, 'buy', DEF);
|
||||
|
||||
if (['GT','LT','GTE','LTE','EQ','NEQ','CROSS_UP','CROSS_DOWN','DIFF_GT','DIFF_LT'].includes(newCondType)) {
|
||||
if (!isValid(updated.leftField)) updated.leftField = def.l;
|
||||
@@ -761,7 +799,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
|
||||
const condOpts = normalized.indicatorType === 'ICHIMOKU' ? ICHIMOKU_CONDS : COND_OPTIONS;
|
||||
|
||||
const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
|
||||
const defaults = getDefaultFields(normalized.indicatorType, signalType, def);
|
||||
const defaults = getDefaultConditionFields(normalized.indicatorType, signalType, def);
|
||||
|
||||
const getLeftValue = () => {
|
||||
const v = resolveFieldOptionValue(normalized.indicatorType, normalized.leftField);
|
||||
@@ -791,7 +829,10 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
|
||||
ZERO_LINE: 0,
|
||||
};
|
||||
let upd: ConditionDSL = { ...normalized, rightField: v };
|
||||
if (thresholds[v] !== undefined) upd.targetValue = thresholds[v];
|
||||
if (thresholds[v] !== undefined) {
|
||||
upd.targetValue = thresholds[v];
|
||||
upd.thresholdOverride = true;
|
||||
}
|
||||
const priorP = parsePriorExtremePeriod(v);
|
||||
if (priorP != null && isPriceExtremeIndicator(normalized.indicatorType)) {
|
||||
upd = syncPriceExtremeSimpleFields({ ...upd, period: priorP });
|
||||
@@ -816,9 +857,9 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
|
||||
?? parsePeriodFromCompositeField(normalized.indicatorType, field);
|
||||
if (p == null) return;
|
||||
if (side === 'left') {
|
||||
onChange(syncCompositeFields({ ...normalized, leftPeriod: p, leftField: field }));
|
||||
onChange(syncCompositeFields({ ...normalized, leftPeriod: p, leftField: field, valuePeriodOverride: true }));
|
||||
} else {
|
||||
onChange(syncCompositeFields({ ...normalized, rightPeriod: p, rightField: field }));
|
||||
onChange(syncCompositeFields({ ...normalized, rightPeriod: p, rightField: field, rightPeriodOverride: true }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -954,7 +995,7 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
|
||||
const base = bases[normalized.indicatorType];
|
||||
if (base && v === base) {
|
||||
const p = getConditionValuePeriod(normalized, def);
|
||||
onChange({ ...normalized, leftField: `${base}_${p}`, period: p });
|
||||
onChange({ ...normalized, leftField: `${base}_${p}`, period: p, valuePeriodOverride: true });
|
||||
} else {
|
||||
onChange({ ...normalized, leftField: v });
|
||||
}
|
||||
@@ -1077,8 +1118,7 @@ export const makeNode = (
|
||||
}
|
||||
return { id: genId(), type: 'CONDITION', condition };
|
||||
}
|
||||
const def = getDefaultFields(value, signalType, DEF);
|
||||
const period = options?.period ?? getDefaultIndicatorPeriod(value, DEF);
|
||||
const def = getDefaultConditionFields(value, signalType, DEF);
|
||||
let conditionType = signalType === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN';
|
||||
if (value === 'NEW_HIGH' && signalType === 'buy') conditionType = 'CROSS_UP';
|
||||
if (value === 'NEW_LOW' && signalType === 'sell') conditionType = 'CROSS_DOWN';
|
||||
@@ -1088,9 +1128,11 @@ export const makeNode = (
|
||||
leftField: def.l,
|
||||
rightField: def.r,
|
||||
candleRange: 1,
|
||||
period,
|
||||
valuePeriodOverride: false,
|
||||
thresholdOverride: false,
|
||||
rightPeriodOverride: false,
|
||||
};
|
||||
const condition = initConditionPeriods(value, baseCondition, DEF);
|
||||
const condition = initConditionPeriodsInherit(value, baseCondition, DEF);
|
||||
return {
|
||||
id: genId(), type: 'CONDITION',
|
||||
condition: isPriceExtremeIndicator(value)
|
||||
|
||||
@@ -20,6 +20,12 @@ export interface ConditionDSL {
|
||||
slopePeriod?: number;
|
||||
holdDays?: number;
|
||||
candleRange?: number;
|
||||
/** false=보조지표 설정 기본값 상속, true=전략 조건 전용 기간 */
|
||||
valuePeriodOverride?: boolean;
|
||||
/** 복합지표 요소2 — false=보조지표 설정 기본값 상속 */
|
||||
rightPeriodOverride?: boolean;
|
||||
/** false=보조지표 hline 기본값 상속, true=전략 조건 전용 임계값 */
|
||||
thresholdOverride?: boolean;
|
||||
}
|
||||
|
||||
export interface LogicNode {
|
||||
|
||||
Reference in New Issue
Block a user