539 lines
18 KiB
TypeScript
539 lines
18 KiB
TypeScript
/** 조건 노드별 지표 기간·임계값 — 차트 DEF 기본값 대비 노드 단위 오버라이드 */
|
|
import type { ConditionDSL } from './strategyTypes';
|
|
import {
|
|
compositeFieldLabel,
|
|
compositeValueField,
|
|
getCompositeDefaultPeriods,
|
|
getDonchianFieldOpts,
|
|
isDonchianComposite,
|
|
isThresholdField,
|
|
parseDonchianPeriod,
|
|
parsePeriodFromCompositeField,
|
|
syncCompositeFields,
|
|
type CompositePeriodDef,
|
|
} from './compositeIndicators';
|
|
import {
|
|
isPriceExtremeIndicator,
|
|
migratePriceExtremeCondition,
|
|
parsePriorExtremePeriod,
|
|
syncPriceExtremeSimpleFields,
|
|
} from './priceExtremeIndicators';
|
|
import {
|
|
isThresholdFieldOrSymbol,
|
|
isThresholdSymbol,
|
|
inferThresholdRoleForValue,
|
|
resolveInheritedThresholdField,
|
|
resolveThresholdFromDef,
|
|
THRESHOLD_HL_MID,
|
|
THRESHOLD_HL_OVER,
|
|
THRESHOLD_HL_UNDER,
|
|
} from './thresholdSymbols';
|
|
import { DEFAULT_START_CANDLE, normalizeStartCandleType } from './strategyStartNodes';
|
|
import {
|
|
getIndicatorPrimaryPeriod,
|
|
formatIndicatorPeriodLabel,
|
|
} from './strategyIndicatorPeriods';
|
|
import type { DefType } from './strategyEditorShared';
|
|
|
|
export interface IndicatorPeriodDef {
|
|
rsiPeriod: number;
|
|
cciPeriod: number;
|
|
adxPeriod: number;
|
|
williamsR: number;
|
|
trixPeriod: number;
|
|
vrPeriod: number;
|
|
newPsy: number;
|
|
psyPeriod: number;
|
|
investPsy: number;
|
|
}
|
|
|
|
export const VALUE_FIELD_PREFIX: 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',
|
|
};
|
|
|
|
export function getDefaultIndicatorPeriod(
|
|
indicatorType: string,
|
|
def: IndicatorPeriodDef | DefType,
|
|
): number {
|
|
return getIndicatorPrimaryPeriod(indicatorType, def as DefType);
|
|
}
|
|
|
|
/** @internal DefType 전체 필드 — 팔레트 라벨 등 */
|
|
export function getDefaultIndicatorPeriodLabel(
|
|
indicatorType: string,
|
|
def: IndicatorPeriodDef | DefType,
|
|
): string {
|
|
return formatIndicatorPeriodLabel(indicatorType, def as DefType);
|
|
}
|
|
|
|
function parseFieldPeriod(field?: string): number | null {
|
|
if (!field || isThresholdField(field)) return null;
|
|
const m = field.match(/_(\d+)$/);
|
|
if (!m) return null;
|
|
const n = parseInt(m[1], 10);
|
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
}
|
|
|
|
/** 조건 노드의 RSI 등 값(좌측) 기간 — true=전략 전용, false/미설정=보조지표 설정(DEF) 상속 */
|
|
export function isValuePeriodOverridden(cond: ConditionDSL): boolean {
|
|
return cond.valuePeriodOverride === true;
|
|
}
|
|
|
|
export function isRightPeriodOverridden(cond: ConditionDSL): boolean {
|
|
return cond.rightPeriodOverride === true;
|
|
}
|
|
|
|
export function isThresholdOverridden(cond: ConditionDSL): boolean {
|
|
return cond.thresholdOverride === true;
|
|
}
|
|
|
|
/** 조건 노드의 RSI 등 값(좌측) 기간 */
|
|
export function getConditionValuePeriod(cond: ConditionDSL, def: IndicatorPeriodDef): number {
|
|
if (isPriceExtremeIndicator(cond.indicatorType)) {
|
|
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 (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;
|
|
}
|
|
return getDefaultIndicatorPeriod(cond.indicatorType, def);
|
|
}
|
|
|
|
export function getConditionRightPeriod(cond: ConditionDSL, def: IndicatorPeriodDef): number {
|
|
if (cond.composite) {
|
|
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;
|
|
}
|
|
return getDefaultIndicatorPeriod(cond.indicatorType, def);
|
|
}
|
|
|
|
/** 조건대상 직접입력 — 기간(일) 또는 임계값 */
|
|
export function resolveFieldCustomKind(
|
|
indicatorType: string,
|
|
field: string | undefined,
|
|
): 'period' | 'threshold' | 'none' {
|
|
if (!field) return 'none';
|
|
if (isThresholdField(field)) return 'threshold';
|
|
const prefix = VALUE_FIELD_PREFIX[indicatorType];
|
|
if (prefix && (field === prefix || field.startsWith(`${prefix}_`))) return 'period';
|
|
return 'none';
|
|
}
|
|
|
|
const INDICATORS_WITH_RIGHT_THRESHOLD_INPUT = new Set([
|
|
'RSI', 'STOCHASTIC', 'CCI', 'ADX', 'DMI', 'WILLIAMS_R', 'TRIX',
|
|
'PSYCHOLOGICAL', 'NEW_PSYCHOLOGICAL', 'INVEST_PSYCHOLOGICAL', 'BWI', 'VR',
|
|
'VOLUME_OSC', 'DISPARITY',
|
|
]);
|
|
|
|
/** 조건대상2 — 임계값 직접입력 노출 여부 */
|
|
export function resolveRightFieldCustomKind(
|
|
indicatorType: string,
|
|
field: string | undefined,
|
|
): 'period' | 'threshold' | 'none' {
|
|
const base = resolveFieldCustomKind(indicatorType, field);
|
|
if (base !== 'none') return base;
|
|
if (parseThresholdField(field) != null) return 'threshold';
|
|
if (isThresholdFieldOrSymbol(field)) return 'threshold';
|
|
if (INDICATORS_WITH_RIGHT_THRESHOLD_INPUT.has(indicatorType)) return 'threshold';
|
|
return 'none';
|
|
}
|
|
|
|
export function isValuePeriodFieldStored(indicatorType: string, field?: string): boolean {
|
|
const prefix = VALUE_FIELD_PREFIX[indicatorType];
|
|
if (!prefix || !field) return false;
|
|
return field.startsWith(`${prefix}_`) && field.length > prefix.length + 1;
|
|
}
|
|
|
|
export function usesValuePeriodField(cond: ConditionDSL): boolean {
|
|
if (isPriceExtremeIndicator(cond.indicatorType)) return true;
|
|
if (cond.composite) return true;
|
|
const prefix = VALUE_FIELD_PREFIX[cond.indicatorType];
|
|
if (!prefix) return false;
|
|
const lf = cond.leftField ?? '';
|
|
return lf === prefix || lf.startsWith(`${prefix}_`);
|
|
}
|
|
|
|
export function parseThresholdField(field?: string): number | null {
|
|
if (!field?.startsWith('K_')) return null;
|
|
const n = parseFloat(field.slice(2));
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
|
|
export function thresholdField(value: number): string {
|
|
return `K_${value}`;
|
|
}
|
|
|
|
export function hasEditableThreshold(cond: ConditionDSL): boolean {
|
|
return isThresholdFieldOrSymbol(cond.rightField)
|
|
|| isThresholdFieldOrSymbol(cond.leftField)
|
|
|| parseThresholdField(cond.rightField) != null;
|
|
}
|
|
|
|
/** 보조지표 차트(DB) hline 기준값 — 전략 오버라이드와 무관하게 항상 DEF에서 조회 */
|
|
export function getChartReferenceThreshold(
|
|
cond: ConditionDSL,
|
|
side: 'left' | 'right',
|
|
inheritedField: string | undefined,
|
|
def: { hlThresh: Record<string, { over?: number; mid?: number; under?: number }> },
|
|
): number | null {
|
|
const raw = side === 'left' ? cond.leftField : (inheritedField ?? cond.rightField);
|
|
if (!raw) return null;
|
|
const field = isThresholdOverridden(cond)
|
|
? raw
|
|
: resolveInheritedThresholdField(raw, cond.indicatorType, def.hlThresh);
|
|
if (!field) return null;
|
|
return resolveThresholdFromDef(field, cond.indicatorType, def.hlThresh);
|
|
}
|
|
|
|
/**
|
|
* 전략 평가용 임계값 — thresholdOverride=true 이면 targetValue/K_, 아니면 차트 DEF.
|
|
*/
|
|
export function getConditionThreshold(
|
|
cond: ConditionDSL,
|
|
side: 'left' | 'right',
|
|
inheritedField?: string,
|
|
def?: { hlThresh: Record<string, { over?: number; mid?: number; under?: number }> },
|
|
): number | null {
|
|
if (isThresholdOverridden(cond) && cond.targetValue != null && Number.isFinite(cond.targetValue)) {
|
|
return cond.targetValue;
|
|
}
|
|
if (!isThresholdOverridden(cond)) {
|
|
const rawField = inheritedField ?? (side === 'left' ? cond.leftField : cond.rightField);
|
|
const roleField = def
|
|
? resolveInheritedThresholdField(rawField, cond.indicatorType, def.hlThresh)
|
|
: rawField;
|
|
if (def && roleField) {
|
|
const fromDef = resolveThresholdFromDef(roleField, cond.indicatorType, def.hlThresh);
|
|
if (fromDef != null) return fromDef;
|
|
}
|
|
if (inheritedField && def) {
|
|
const inheritedRole = resolveInheritedThresholdField(
|
|
inheritedField,
|
|
cond.indicatorType,
|
|
def.hlThresh,
|
|
);
|
|
const fromInherited = resolveThresholdFromDef(
|
|
inheritedRole,
|
|
cond.indicatorType,
|
|
def.hlThresh,
|
|
);
|
|
if (fromInherited != null) return fromInherited;
|
|
}
|
|
}
|
|
const field = side === 'left' ? cond.leftField : cond.rightField;
|
|
return parseThresholdField(field);
|
|
}
|
|
|
|
export type SetConditionThresholdOptions = {
|
|
/** true: 직접입력 — K_+targetValue+thresholdOverride 유지 (프리셋 HL_* 로 접지 않음) */
|
|
directInput?: boolean;
|
|
};
|
|
|
|
export function activateDirectThresholdInput(
|
|
cond: ConditionDSL,
|
|
side: 'left' | 'right',
|
|
inheritedField: string | undefined,
|
|
def: { hlThresh: Record<string, { over?: number; mid?: number; under?: number }> },
|
|
): ConditionDSL {
|
|
const value = getConditionThreshold(cond, side, inheritedField, def)
|
|
?? getChartReferenceThreshold(cond, side, inheritedField, def)
|
|
?? parseThresholdField(side === 'right' ? cond.rightField : cond.leftField);
|
|
if (value == null || !Number.isFinite(value)) return cond;
|
|
return setConditionThreshold(cond, side, value, def, { directInput: true });
|
|
}
|
|
|
|
export function setConditionThreshold(
|
|
cond: ConditionDSL,
|
|
side: 'left' | 'right',
|
|
value: number,
|
|
def?: { hlThresh: Record<string, { over?: number; mid?: number; under?: number }> },
|
|
options?: SetConditionThresholdOptions,
|
|
): ConditionDSL {
|
|
const fieldKey = side === 'left' ? 'leftField' : 'rightField';
|
|
const current = cond[fieldKey];
|
|
if (!isThresholdFieldOrSymbol(current)) {
|
|
if (side === 'right' && INDICATORS_WITH_RIGHT_THRESHOLD_INPUT.has(cond.indicatorType)) {
|
|
return {
|
|
...cond,
|
|
[fieldKey]: thresholdField(value),
|
|
targetValue: value,
|
|
thresholdOverride: true,
|
|
};
|
|
}
|
|
return cond;
|
|
}
|
|
|
|
const inheritedField = side === 'right'
|
|
? (current?.startsWith('K_') || isThresholdSymbol(current) ? current : THRESHOLD_HL_OVER)
|
|
: current;
|
|
const chartRef = def
|
|
? getChartReferenceThreshold(cond, side, inheritedField, def)
|
|
: null;
|
|
|
|
if (!options?.directInput) {
|
|
const matchedRole = def
|
|
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
|
|
: null;
|
|
if (matchedRole != null) {
|
|
const { targetValue: _t, ...rest } = cond;
|
|
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
|
}
|
|
|
|
if (chartRef != null && Math.abs(chartRef - value) < 0.0001) {
|
|
const role = isThresholdSymbol(inheritedField)
|
|
? inheritedField
|
|
: (side === 'right' ? THRESHOLD_HL_OVER : inheritedField);
|
|
const { targetValue: _t, ...rest } = cond;
|
|
return { ...rest, [fieldKey]: role, thresholdOverride: false };
|
|
}
|
|
}
|
|
|
|
return {
|
|
...cond,
|
|
[fieldKey]: thresholdField(value),
|
|
targetValue: value,
|
|
thresholdOverride: true,
|
|
};
|
|
}
|
|
|
|
export function setConditionValuePeriod(
|
|
cond: ConditionDSL,
|
|
period: number,
|
|
_def: IndicatorPeriodDef,
|
|
): ConditionDSL {
|
|
const p = Math.max(1, Math.min(500, Math.round(period)));
|
|
if (isPriceExtremeIndicator(cond.indicatorType)) {
|
|
return syncPriceExtremeSimpleFields({ ...cond, period: p, valuePeriodOverride: true });
|
|
}
|
|
if (cond.composite) {
|
|
return syncCompositeFields({ ...cond, composite: true, leftPeriod: p, valuePeriodOverride: true });
|
|
}
|
|
const prefix = VALUE_FIELD_PREFIX[cond.indicatorType];
|
|
const next: ConditionDSL = { ...cond, period: p, valuePeriodOverride: true };
|
|
if (prefix && (cond.leftField === prefix || cond.leftField?.startsWith(`${prefix}_`))) {
|
|
next.leftField = `${prefix}_${p}`;
|
|
}
|
|
return next;
|
|
}
|
|
|
|
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, rightPeriodOverride: 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,
|
|
def: IndicatorPeriodDef,
|
|
): ConditionDSL {
|
|
if (isPriceExtremeIndicator(indicatorType)) {
|
|
const period = condition.period ?? getDefaultIndicatorPeriod(indicatorType, def);
|
|
return syncPriceExtremeSimpleFields({ ...condition, period });
|
|
}
|
|
if (condition.composite) return condition;
|
|
const prefix = VALUE_FIELD_PREFIX[indicatorType];
|
|
if (!prefix) return condition;
|
|
const period = getDefaultIndicatorPeriod(indicatorType, def);
|
|
const lf = condition.leftField ?? '';
|
|
if (lf === prefix || lf.startsWith(`${prefix}_`)) {
|
|
return { ...condition, period: condition.period ?? period, leftField: `${prefix}_${condition.period ?? period}` };
|
|
}
|
|
return { ...condition, period: condition.period ?? period };
|
|
}
|
|
|
|
export function hasNodeSettings(cond: ConditionDSL): boolean {
|
|
return usesValuePeriodField(cond)
|
|
|| hasEditableThreshold(cond)
|
|
|| !!cond.composite;
|
|
}
|
|
|
|
const COMMON_PERIODS = [5, 7, 9, 10, 12, 13, 14, 20, 21, 26, 28, 50, 60, 120];
|
|
|
|
export function getPeriodPresetOptions(indicatorType: string, def: IndicatorPeriodDef): number[] {
|
|
const base = getDefaultIndicatorPeriod(indicatorType, def);
|
|
if (isPriceExtremeIndicator(indicatorType)) {
|
|
return [...new Set([base, 5, 9, 10, 20, 55, ...COMMON_PERIODS])].sort((a, b) => a - b);
|
|
}
|
|
return [...new Set([base, ...COMMON_PERIODS])].sort((a, b) => a - b);
|
|
}
|
|
|
|
export function getPeriodSettingsLabel(cond: ConditionDSL): string | null {
|
|
if (isPriceExtremeIndicator(cond.indicatorType)) {
|
|
return cond.indicatorType === 'NEW_LOW' ? '신저가 기간 (봉)' : '신고가 기간 (봉)';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 복합지표 요소1(단기)·요소2(장기) 기간 프리셋 */
|
|
export function getCompositePeriodPresetOptions(
|
|
indicatorType: string,
|
|
def: IndicatorPeriodDef,
|
|
side: 'left' | 'right',
|
|
): number[] {
|
|
const { short, long } = getCompositeDefaultPeriods(indicatorType, def as CompositePeriodDef);
|
|
const base = side === 'left' ? short : long;
|
|
return [...new Set([base, short, long, ...COMMON_PERIODS])].sort((a, b) => a - b);
|
|
}
|
|
|
|
/** 복합지표 조건대상1/2 드롭다운 — CCI 1 라인(9일) 형식 */
|
|
export function getCompositeLeftCandleType(cond: ConditionDSL): string {
|
|
return normalizeStartCandleType(cond.leftCandleType ?? DEFAULT_START_CANDLE);
|
|
}
|
|
|
|
export function getCompositeRightCandleType(cond: ConditionDSL): string {
|
|
return normalizeStartCandleType(cond.rightCandleType ?? DEFAULT_START_CANDLE);
|
|
}
|
|
|
|
export function setCompositeLeftCandleType(cond: ConditionDSL, candleType: string): ConditionDSL {
|
|
return syncCompositeFields({
|
|
...cond,
|
|
composite: true,
|
|
leftCandleType: normalizeStartCandleType(candleType),
|
|
});
|
|
}
|
|
|
|
export function setCompositeRightCandleType(cond: ConditionDSL, candleType: string): ConditionDSL {
|
|
return syncCompositeFields({
|
|
...cond,
|
|
composite: true,
|
|
rightCandleType: normalizeStartCandleType(candleType),
|
|
});
|
|
}
|
|
|
|
export function getCompositeFieldOpts(
|
|
indicatorType: string,
|
|
slot: 1 | 2,
|
|
def: IndicatorPeriodDef,
|
|
cond: ConditionDSL,
|
|
): { value: string; label: string }[] {
|
|
const side = slot === 1 ? 'left' : 'right';
|
|
const current = slot === 1
|
|
? getConditionValuePeriod(cond, def)
|
|
: getConditionRightPeriod(cond, def);
|
|
const presets = getCompositePeriodPresetOptions(indicatorType, def, side);
|
|
const periods = [...new Set([...presets, current])].sort((a, b) => a - b);
|
|
|
|
if (isDonchianComposite(indicatorType)) {
|
|
const opts = getDonchianFieldOpts(periods);
|
|
const cur = slot === 1 ? cond.leftField : cond.rightField;
|
|
if (cur && !opts.some(o => o.value === cur)) {
|
|
const p = parseDonchianPeriod(cur);
|
|
const label = p != null ? compositeFieldLabel(indicatorType, slot, p) : cur;
|
|
return [{ value: cur, label }, ...opts];
|
|
}
|
|
return opts;
|
|
}
|
|
|
|
return periods.map(p => ({
|
|
value: compositeValueField(indicatorType, p),
|
|
label: compositeFieldLabel(indicatorType, slot, p),
|
|
}));
|
|
}
|
|
|
|
export function getThresholdPresetOptions(indicatorType: string): number[] {
|
|
switch (indicatorType) {
|
|
case 'RSI':
|
|
case 'PSYCHOLOGICAL':
|
|
case 'INVEST_PSYCHOLOGICAL':
|
|
return [20, 25, 30, 50, 70, 75, 80];
|
|
case 'NEW_PSYCHOLOGICAL':
|
|
return [-50, -25, 0, 25, 50];
|
|
case 'STOCHASTIC':
|
|
return [20, 50, 80];
|
|
case 'CCI':
|
|
return [-200, -100, -50, 0, 50, 100, 150, 200];
|
|
case 'WILLIAMS_R':
|
|
return [-80, -50, -20];
|
|
case 'ADX':
|
|
case 'DMI':
|
|
return [20, 25, 30, 40, 50];
|
|
case 'VR':
|
|
return [70, 100, 150, 200, 250];
|
|
default:
|
|
return [-100, -50, 0, 50, 100];
|
|
}
|
|
}
|
|
|
|
export function getThresholdBounds(indicatorType: string): { min: number; max: number } {
|
|
switch (indicatorType) {
|
|
case 'RSI':
|
|
case 'PSYCHOLOGICAL':
|
|
case 'INVEST_PSYCHOLOGICAL':
|
|
case 'STOCHASTIC':
|
|
return { min: 0, max: 100 };
|
|
case 'NEW_PSYCHOLOGICAL':
|
|
return { min: -100, max: 100 };
|
|
case 'WILLIAMS_R':
|
|
return { min: -100, max: 0 };
|
|
case 'CCI':
|
|
return { min: -500, max: 500 };
|
|
default:
|
|
return { min: -1000, max: 1000 };
|
|
}
|
|
}
|