직접입력 평가로직 오류

This commit is contained in:
Macbook
2026-06-17 22:10:43 +09:00
parent 3c269db6b7
commit 40cae91d8d
6 changed files with 253 additions and 10 deletions
@@ -94,7 +94,6 @@ export default function ComboFieldSelect({
if (raw === COMBO_FIELD_CUSTOM) {
if (canCustom) {
setMode('custom');
onCustomNumberChange(customNumber);
setDraft(String(customNumber));
}
return;
+9
View File
@@ -21,6 +21,7 @@ import {
import {
isThresholdFieldOrSymbol,
isThresholdSymbol,
inferThresholdRoleForValue,
resolveInheritedThresholdField,
resolveThresholdFromDef,
THRESHOLD_HL_MID,
@@ -282,6 +283,14 @@ export function setConditionThreshold(
? getChartReferenceThreshold(cond, side, inheritedField, def)
: null;
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
@@ -11,6 +11,7 @@ import {
} from './conditionPeriods';
import {
defaultInheritedThresholdField,
inferThresholdRoleForValue,
inferThresholdSymbolFromLegacy,
isThresholdFieldOrSymbol,
isThresholdSymbol,
@@ -95,13 +96,33 @@ export function migrateLogicRootForEditor(
return repairPriceExtremePairForest(migrated.root, migrated.orphans);
}
/** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거 */
/** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거, 직접입력은 hline 역할로 등가 정규화 */
export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL {
let next: ConditionDSL = { ...cond };
if (isThresholdOverridden(next)) {
if (next.targetValue != null && Number.isFinite(next.targetValue)) {
const role = inferThresholdRoleForValue(next.targetValue, next.indicatorType, {});
if (role != null) {
const { targetValue: _t, ...rest } = next;
next = { ...rest, rightField: role, thresholdOverride: false };
}
} else if (next.rightField?.startsWith('K_')) {
const role = inferThresholdSymbolFromLegacy(next.rightField, next.indicatorType, {});
if (role && isThresholdSymbol(role)) {
const { targetValue: _t, ...rest } = next;
next = { ...rest, rightField: role, thresholdOverride: false };
}
}
}
if (!isThresholdOverridden(next)) {
if (next.rightField?.startsWith('K_')) {
next.rightField = defaultInheritedThresholdField(next.indicatorType);
next.rightField = inferThresholdSymbolFromLegacy(
next.rightField,
next.indicatorType,
{},
) ?? defaultInheritedThresholdField(next.indicatorType);
}
if (!isThresholdSymbol(next.rightField)) {
const role = next.indicatorType === 'DISPARITY' || next.indicatorType === 'VOLUME_OSC'
+10 -2
View File
@@ -101,6 +101,7 @@ import {
THRESHOLD_HL_MID,
THRESHOLD_HL_OVER,
THRESHOLD_HL_UNDER,
defaultThresholdForRole,
isThresholdFieldOrSymbol,
isThresholdSymbol,
resolveInheritedThresholdField,
@@ -846,11 +847,16 @@ export const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, D
}
const fieldOpts = getFieldOpts(cond.indicatorType, 'buy', DEF);
const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
const isValidRight = (v: string|undefined) => {
if (!v) return false;
if (isThresholdOverridden(cond) && (v.startsWith('K_') || isThresholdSymbol(v))) return true;
return isValid(v);
};
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;
if (!isValid(updated.rightField)) updated.rightField = def.r;
if (!isValidRight(updated.rightField)) updated.rightField = def.r;
if (['DIFF_GT','DIFF_LT'].includes(newCondType)) updated.compareValue = updated.compareValue ?? 0;
} else if (['SLOPE_UP','SLOPE_DOWN'].includes(newCondType)) {
if (!isValid(updated.leftField)) updated.leftField = def.l;
@@ -1417,7 +1423,9 @@ export const CondEditor: React.FC<CondEditorProps> = ({
customNumber={getConditionThreshold(normalized, 'right', inheritedThresholdField, def)
?? getChartReferenceThreshold(normalized, 'right', inheritedThresholdField, def)
?? parseThresholdField(normalized.rightField)
?? 0}
?? defaultThresholdForRole(normalized.indicatorType, THRESHOLD_HL_MID, def.hlThresh)
?? defaultThresholdForRole(normalized.indicatorType, THRESHOLD_HL_OVER, def.hlThresh)
?? 50}
numberPresets={getThresholdPresetOptions(normalized.indicatorType)}
min={getThresholdBounds(normalized.indicatorType).min}
max={getThresholdBounds(normalized.indicatorType).max}
+57 -5
View File
@@ -15,16 +15,53 @@ export function isThresholdFieldOrSymbol(field?: string): boolean {
type HlThresh = { over?: number; mid?: number; under?: number };
/** Ta4j IndicatorHlineResolver.defaultForRole 와 동일 — hlThresh 미설정 시 폴백 */
export const DEFAULT_HL_THRESH: Record<string, HlThresh> = {
RSI: { over: 70, mid: 50, under: 30 },
STOCHASTIC: { over: 80, mid: 50, under: 20 },
CCI: { over: 100, mid: 0, under: -100 },
WILLIAMS_R: { over: -20, mid: -50, under: -80 },
BWI: { over: 80, mid: 50, under: 20 },
VR: { over: 200, mid: 100, under: 50 },
PSYCHOLOGICAL: { over: 75, mid: 50, under: 25 },
NEW_PSYCHOLOGICAL: { over: 50, mid: 0, under: -50 },
INVEST_PSYCHOLOGICAL:{ over: 75, mid: 50, under: 25 },
MACD: { mid: 0 },
ADX: { over: 40, mid: 25, under: 20 },
DMI: { over: 30, mid: 20, under: 10 },
TRIX: { mid: 0 },
VOLUME_OSC: { mid: 0 },
DISPARITY: { mid: 100 },
};
function mergedHlThresh(
indicatorType: string,
hlThresh: Record<string, HlThresh>,
): HlThresh {
return { ...DEFAULT_HL_THRESH[indicatorType], ...hlThresh[indicatorType] };
}
export function defaultThresholdForRole(
indicatorType: string,
role: ThresholdHlineRole,
hlThresh: Record<string, HlThresh> = {},
): number | null {
const th = mergedHlThresh(indicatorType, hlThresh);
if (role === THRESHOLD_HL_OVER) return th.over ?? null;
if (role === THRESHOLD_HL_MID) return th.mid ?? null;
if (role === THRESHOLD_HL_UNDER) return th.under ?? null;
return null;
}
export function resolveThresholdFromDef(
field: string | undefined,
indicatorType: string,
hlThresh: Record<string, HlThresh>,
): number | null {
if (!field) return null;
const th = hlThresh[indicatorType] ?? {};
if (field === THRESHOLD_HL_OVER) return th.over ?? null;
if (field === THRESHOLD_HL_MID) return th.mid ?? null;
if (field === THRESHOLD_HL_UNDER) return th.under ?? null;
if (field === THRESHOLD_HL_OVER) return defaultThresholdForRole(indicatorType, THRESHOLD_HL_OVER, hlThresh);
if (field === THRESHOLD_HL_MID) return defaultThresholdForRole(indicatorType, THRESHOLD_HL_MID, hlThresh);
if (field === THRESHOLD_HL_UNDER) return defaultThresholdForRole(indicatorType, THRESHOLD_HL_UNDER, hlThresh);
if (field.startsWith('K_')) {
const n = parseFloat(field.slice(2));
return Number.isFinite(n) ? n : null;
@@ -41,7 +78,7 @@ export function inferThresholdSymbolFromLegacy(
if (!field?.startsWith('K_')) return field;
const val = parseFloat(field.slice(2));
if (!Number.isFinite(val)) return field;
const th = hlThresh[indicatorType] ?? {};
const th = mergedHlThresh(indicatorType, hlThresh);
const eps = 0.0001;
if (th.over != null && Math.abs(th.over - val) < eps) return THRESHOLD_HL_OVER;
if (th.mid != null && Math.abs(th.mid - val) < eps) return THRESHOLD_HL_MID;
@@ -80,6 +117,21 @@ export function resolveInheritedThresholdField(
return field;
}
/** K_ 숫자값이 hline 역할과 일치하면 역할 심볼 반환 (직접입력 ↔ 중앙선 등가) */
export function inferThresholdRoleForValue(
value: number,
indicatorType: string,
hlThresh: Record<string, HlThresh> = {},
): ThresholdHlineRole | null {
if (!Number.isFinite(value)) return null;
const th = mergedHlThresh(indicatorType, hlThresh);
const eps = 0.0001;
if (th.over != null && Math.abs(th.over - value) < eps) return THRESHOLD_HL_OVER;
if (th.mid != null && Math.abs(th.mid - value) < eps) return THRESHOLD_HL_MID;
if (th.under != null && Math.abs(th.under - value) < eps) return THRESHOLD_HL_UNDER;
return null;
}
/** 상속 모드 기본 rightField — 과열선 역할 */
export function defaultInheritedThresholdField(indicatorType: string): string {
switch (indicatorType) {