전략 직접입력값 평가오류

This commit is contained in:
Macbook
2026-06-17 23:17:17 +09:00
parent 40cae91d8d
commit 5236a6cd46
10 changed files with 88 additions and 40 deletions
@@ -20,6 +20,11 @@ public final class IndicatorHlineResolver {
if (field == null || field.isBlank()) return null; if (field == null || field.isBlank()) return null;
// K_ 직접입력 — targetValue 가 있으면 thresholdOverride 플래그와 무관하게 우선
if (field.startsWith("K_") && cond != null && cond.has("targetValue") && !cond.path("targetValue").isNull()) {
return cond.path("targetValue").asDouble();
}
boolean override = cond != null && cond.path("thresholdOverride").asBoolean(false); boolean override = cond != null && cond.path("thresholdOverride").asBoolean(false);
if (override) { if (override) {
if (cond.has("targetValue") && !cond.path("targetValue").isNull()) { if (cond.has("targetValue") && !cond.path("targetValue").isNull()) {
@@ -18,6 +18,8 @@ interface Props {
allowDecimal?: boolean; allowDecimal?: boolean;
onFieldChange: (field: string) => void; onFieldChange: (field: string) => void;
onCustomNumberChange: (n: number) => void; onCustomNumberChange: (n: number) => void;
/** 직접입력 모드 진입 — 현재 임계값을 K_ 스냅샷으로 즉시 반영 */
onDirectInputActivate?: () => void;
customOptionLabel?: string; customOptionLabel?: string;
disabled?: boolean; disabled?: boolean;
} }
@@ -66,6 +68,7 @@ export default function ComboFieldSelect({
allowDecimal = false, allowDecimal = false,
onFieldChange, onFieldChange,
onCustomNumberChange, onCustomNumberChange,
onDirectInputActivate,
customOptionLabel, customOptionLabel,
disabled = false, disabled = false,
}: Props) { }: Props) {
@@ -94,6 +97,7 @@ export default function ComboFieldSelect({
if (raw === COMBO_FIELD_CUSTOM) { if (raw === COMBO_FIELD_CUSTOM) {
if (canCustom) { if (canCustom) {
setMode('custom'); setMode('custom');
onDirectInputActivate?.();
setDraft(String(customNumber)); setDraft(String(customNumber));
} }
return; return;
@@ -168,7 +168,7 @@ export default function ConditionNodeSettings({
min={thresholdBounds.min} min={thresholdBounds.min}
max={thresholdBounds.max} max={thresholdBounds.max}
allowDecimal allowDecimal
onChange={v => onChange(setConditionThreshold(condition, 'right', v, def))} onChange={v => onChange(setConditionThreshold(condition, 'right', v, def, { directInput: true }))}
/> />
</label> </label>
</> </>
+21
View File
@@ -256,11 +256,30 @@ export function getConditionThreshold(
return parseThresholdField(field); 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( export function setConditionThreshold(
cond: ConditionDSL, cond: ConditionDSL,
side: 'left' | 'right', side: 'left' | 'right',
value: number, value: number,
def?: { hlThresh: Record<string, { over?: number; mid?: number; under?: number }> }, def?: { hlThresh: Record<string, { over?: number; mid?: number; under?: number }> },
options?: SetConditionThresholdOptions,
): ConditionDSL { ): ConditionDSL {
const fieldKey = side === 'left' ? 'leftField' : 'rightField'; const fieldKey = side === 'left' ? 'leftField' : 'rightField';
const current = cond[fieldKey]; const current = cond[fieldKey];
@@ -283,6 +302,7 @@ export function setConditionThreshold(
? getChartReferenceThreshold(cond, side, inheritedField, def) ? getChartReferenceThreshold(cond, side, inheritedField, def)
: null; : null;
if (!options?.directInput) {
const matchedRole = def const matchedRole = def
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh) ? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
: null; : null;
@@ -298,6 +318,7 @@ export function setConditionThreshold(
const { targetValue: _t, ...rest } = cond; const { targetValue: _t, ...rest } = cond;
return { ...rest, [fieldKey]: role, thresholdOverride: false }; return { ...rest, [fieldKey]: role, thresholdOverride: false };
} }
}
return { return {
...cond, ...cond,
@@ -11,8 +11,8 @@ import {
} from './conditionPeriods'; } from './conditionPeriods';
import { import {
defaultInheritedThresholdField, defaultInheritedThresholdField,
inferThresholdRoleForValue,
inferThresholdSymbolFromLegacy, inferThresholdSymbolFromLegacy,
inferThresholdSymbolFromLegacyExact,
isThresholdFieldOrSymbol, isThresholdFieldOrSymbol,
isThresholdSymbol, isThresholdSymbol,
THRESHOLD_HL_MID, THRESHOLD_HL_MID,
@@ -29,7 +29,8 @@ function migrateConditionThreshold(
let rightField = cond.rightField; let rightField = cond.rightField;
if (rightField?.startsWith('K_')) { if (rightField?.startsWith('K_')) {
rightField = inferThresholdSymbolFromLegacy(rightField, cond.indicatorType, def.hlThresh); rightField = inferThresholdSymbolFromLegacyExact(rightField, cond.indicatorType, def.hlThresh)
?? inferThresholdSymbolFromLegacy(rightField, cond.indicatorType, def.hlThresh);
} }
if (!rightField || (!isThresholdSymbol(rightField) && !rightField.startsWith('K_'))) { if (!rightField || (!isThresholdSymbol(rightField) && !rightField.startsWith('K_'))) {
const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def); const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def);
@@ -96,29 +97,13 @@ export function migrateLogicRootForEditor(
return repairPriceExtremePairForest(migrated.root, migrated.orphans); return repairPriceExtremePairForest(migrated.root, migrated.orphans);
} }
/** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거, 직접입력은 hline 역할로 등가 정규화 */ /** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거 (직접입력 K_+targetValue 는 유지) */
export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL { export function normalizeConditionForPersistence(cond: ConditionDSL): ConditionDSL {
let next: ConditionDSL = { ...cond }; 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 (!isThresholdOverridden(next)) {
if (next.rightField?.startsWith('K_')) { if (next.rightField?.startsWith('K_')) {
next.rightField = inferThresholdSymbolFromLegacy( next.rightField = inferThresholdSymbolFromLegacyExact(
next.rightField, next.rightField,
next.indicatorType, next.indicatorType,
{}, {},
+6 -1
View File
@@ -21,6 +21,7 @@ import {
import { import {
defaultInheritedThresholdField, defaultInheritedThresholdField,
inferThresholdSymbolFromLegacy, inferThresholdSymbolFromLegacy,
inferThresholdSymbolFromLegacyExact,
} from './thresholdSymbols'; } from './thresholdSymbols';
import type { DefType } from './strategyEditorShared'; import type { DefType } from './strategyEditorShared';
import { isPriceExtremeBreakoutPairRoot } from './priceExtremeBreakoutPair'; import { isPriceExtremeBreakoutPairRoot } from './priceExtremeBreakoutPair';
@@ -69,7 +70,11 @@ function applyGlobalDefToCondition(
} }
if (!isThresholdOverridden(cond) && next.rightField?.startsWith('K_')) { if (!isThresholdOverridden(cond) && next.rightField?.startsWith('K_')) {
next.rightField = inferThresholdSymbolFromLegacy( next.rightField = inferThresholdSymbolFromLegacyExact(
next.rightField,
cond.indicatorType,
def.hlThresh,
) ?? inferThresholdSymbolFromLegacy(
next.rightField, next.rightField,
cond.indicatorType, cond.indicatorType,
def.hlThresh, def.hlThresh,
+10 -1
View File
@@ -73,6 +73,7 @@ import {
} from '../utils/stochOverboughtPair'; } from '../utils/stochOverboughtPair';
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect'; import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
import { import {
activateDirectThresholdInput,
getCompositeFieldOpts, getCompositeFieldOpts,
getCompositePeriodPresetOptions, getCompositePeriodPresetOptions,
getConditionRightPeriod, getConditionRightPeriod,
@@ -1432,7 +1433,15 @@ export const CondEditor: React.FC<CondEditorProps> = ({
allowDecimal={normalized.indicatorType === 'CCI'} allowDecimal={normalized.indicatorType === 'CCI'}
disabled={isSlopeType || isHoldType} disabled={isSlopeType || isHoldType}
onFieldChange={handleRight} onFieldChange={handleRight}
onCustomNumberChange={v => onChange(setConditionThreshold(normalized, 'right', v, def))} onDirectInputActivate={() => {
onChange(activateDirectThresholdInput(
normalized,
'right',
inheritedThresholdField,
def,
));
}}
onCustomNumberChange={v => onChange(setConditionThreshold(normalized, 'right', v, def, { directInput: true }))}
/> />
</div> </div>
{/* 조건 */} {/* 조건 */}
+4 -3
View File
@@ -16,11 +16,12 @@ export interface StrategyIndicatorRef {
/** 조건 rightField·targetValue → 차트에 추가할 숫자 임계 hline (HL_* 는 DB visual 사용) */ /** 조건 rightField·targetValue → 차트에 추가할 숫자 임계 hline (HL_* 는 DB visual 사용) */
export function resolveConditionThresholdForChart(c: ConditionDSL): number | null { export function resolveConditionThresholdForChart(c: ConditionDSL): number | null {
if (c.thresholdOverride === false) return null; const rf = c.rightField ?? '';
const isDirectK = rf.startsWith('K_');
if (c.thresholdOverride === false && !isDirectK) return null;
if (c.targetValue != null && Number.isFinite(c.targetValue)) return c.targetValue; if (c.targetValue != null && Number.isFinite(c.targetValue)) return c.targetValue;
if (c.compareValue != null && Number.isFinite(c.compareValue)) return c.compareValue; if (c.compareValue != null && Number.isFinite(c.compareValue)) return c.compareValue;
const rf = c.rightField ?? ''; if (isDirectK) {
if (rf.startsWith('K_')) {
const n = parseFloat(rf.slice(2)); const n = parseFloat(rf.slice(2));
return Number.isFinite(n) ? n : null; return Number.isFinite(n) ? n : null;
} }
+12
View File
@@ -69,6 +69,18 @@ export function resolveThresholdFromDef(
return null; return null;
} }
/** 레거시 K_ 값 → hline 역할 심볼 (정확 일치만 — 저장·마이그레이션용) */
export function inferThresholdSymbolFromLegacyExact(
field: string | undefined,
indicatorType: string,
hlThresh: Record<string, HlThresh>,
): ThresholdHlineRole | undefined {
if (!field?.startsWith('K_')) return undefined;
const val = parseFloat(field.slice(2));
if (!Number.isFinite(val)) return undefined;
return inferThresholdRoleForValue(val, indicatorType, hlThresh) ?? undefined;
}
/** 레거시 K_ 값 → hline 역할 심볼 (정확 일치 우선, 없으면 가장 가까운 역할) */ /** 레거시 K_ 값 → hline 역할 심볼 (정확 일치 우선, 없으면 가장 가까운 역할) */
export function inferThresholdSymbolFromLegacy( export function inferThresholdSymbolFromLegacy(
field: string | undefined, field: string | undefined,
@@ -8,6 +8,7 @@ import { formatIndicatorDisplayLabel } from './indicatorRegistry';
import { coerceFiniteNumber, safeToFixed } from './safeFormat'; import { coerceFiniteNumber, safeToFixed } from './safeFormat';
import { asLogicNode } from './strategyHydrate'; import { asLogicNode } from './strategyHydrate';
import { normalizeStartCandleType } from './strategyStartNodes'; import { normalizeStartCandleType } from './strategyStartNodes';
import { parseThresholdField } from './conditionPeriods';
export { coerceFiniteNumber } from './safeFormat'; export { coerceFiniteNumber } from './safeFormat';
@@ -65,7 +66,12 @@ function walk(
seen.add(rowId); seen.add(rowId);
const condLabel = CONDITION_LABEL[c.conditionType] ?? c.conditionType; const condLabel = CONDITION_LABEL[c.conditionType] ?? c.conditionType;
const target = coerceFiniteNumber(c.targetValue ?? c.compareValue ?? null); const target = coerceFiniteNumber(
c.targetValue
?? c.compareValue
?? parseThresholdField(c.rightField)
?? null,
);
const plotKey = c.leftField && c.leftField !== 'none' ? c.leftField : c.indicatorType; const plotKey = c.leftField && c.leftField !== 'none' ? c.leftField : c.indicatorType;
out.push({ out.push({