전략 직접입력값 평가오류

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
@@ -18,6 +18,8 @@ interface Props {
allowDecimal?: boolean;
onFieldChange: (field: string) => void;
onCustomNumberChange: (n: number) => void;
/** 직접입력 모드 진입 — 현재 임계값을 K_ 스냅샷으로 즉시 반영 */
onDirectInputActivate?: () => void;
customOptionLabel?: string;
disabled?: boolean;
}
@@ -66,6 +68,7 @@ export default function ComboFieldSelect({
allowDecimal = false,
onFieldChange,
onCustomNumberChange,
onDirectInputActivate,
customOptionLabel,
disabled = false,
}: Props) {
@@ -94,6 +97,7 @@ export default function ComboFieldSelect({
if (raw === COMBO_FIELD_CUSTOM) {
if (canCustom) {
setMode('custom');
onDirectInputActivate?.();
setDraft(String(customNumber));
}
return;
@@ -168,7 +168,7 @@ export default function ConditionNodeSettings({
min={thresholdBounds.min}
max={thresholdBounds.max}
allowDecimal
onChange={v => onChange(setConditionThreshold(condition, 'right', v, def))}
onChange={v => onChange(setConditionThreshold(condition, 'right', v, def, { directInput: true }))}
/>
</label>
</>
+34 -13
View File
@@ -256,11 +256,30 @@ export function getConditionThreshold(
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];
@@ -283,20 +302,22 @@ 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 (!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 };
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 {
@@ -11,8 +11,8 @@ import {
} from './conditionPeriods';
import {
defaultInheritedThresholdField,
inferThresholdRoleForValue,
inferThresholdSymbolFromLegacy,
inferThresholdSymbolFromLegacyExact,
isThresholdFieldOrSymbol,
isThresholdSymbol,
THRESHOLD_HL_MID,
@@ -29,7 +29,8 @@ function migrateConditionThreshold(
let rightField = cond.rightField;
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_'))) {
const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def);
@@ -96,29 +97,13 @@ export function migrateLogicRootForEditor(
return repairPriceExtremePairForest(migrated.root, migrated.orphans);
}
/** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거, 직접입력은 hline 역할로 등가 정규화 */
/** 저장 직전 — 상속 모드는 숫자 스냅샷·targetValue 제거 (직접입력 K_+targetValue 는 유지) */
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 = inferThresholdSymbolFromLegacy(
next.rightField = inferThresholdSymbolFromLegacyExact(
next.rightField,
next.indicatorType,
{},
+6 -1
View File
@@ -21,6 +21,7 @@ import {
import {
defaultInheritedThresholdField,
inferThresholdSymbolFromLegacy,
inferThresholdSymbolFromLegacyExact,
} from './thresholdSymbols';
import type { DefType } from './strategyEditorShared';
import { isPriceExtremeBreakoutPairRoot } from './priceExtremeBreakoutPair';
@@ -69,7 +70,11 @@ function applyGlobalDefToCondition(
}
if (!isThresholdOverridden(cond) && next.rightField?.startsWith('K_')) {
next.rightField = inferThresholdSymbolFromLegacy(
next.rightField = inferThresholdSymbolFromLegacyExact(
next.rightField,
cond.indicatorType,
def.hlThresh,
) ?? inferThresholdSymbolFromLegacy(
next.rightField,
cond.indicatorType,
def.hlThresh,
+10 -1
View File
@@ -73,6 +73,7 @@ import {
} from '../utils/stochOverboughtPair';
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
import {
activateDirectThresholdInput,
getCompositeFieldOpts,
getCompositePeriodPresetOptions,
getConditionRightPeriod,
@@ -1432,7 +1433,15 @@ export const CondEditor: React.FC<CondEditorProps> = ({
allowDecimal={normalized.indicatorType === 'CCI'}
disabled={isSlopeType || isHoldType}
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>
{/* 조건 */}
+4 -3
View File
@@ -16,11 +16,12 @@ export interface StrategyIndicatorRef {
/** 조건 rightField·targetValue → 차트에 추가할 숫자 임계 hline (HL_* 는 DB visual 사용) */
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.compareValue != null && Number.isFinite(c.compareValue)) return c.compareValue;
const rf = c.rightField ?? '';
if (rf.startsWith('K_')) {
if (isDirectK) {
const n = parseFloat(rf.slice(2));
return Number.isFinite(n) ? n : null;
}
+12
View File
@@ -69,6 +69,18 @@ export function resolveThresholdFromDef(
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 역할 심볼 (정확 일치 우선, 없으면 가장 가까운 역할) */
export function inferThresholdSymbolFromLegacy(
field: string | undefined,
@@ -8,6 +8,7 @@ import { formatIndicatorDisplayLabel } from './indicatorRegistry';
import { coerceFiniteNumber, safeToFixed } from './safeFormat';
import { asLogicNode } from './strategyHydrate';
import { normalizeStartCandleType } from './strategyStartNodes';
import { parseThresholdField } from './conditionPeriods';
export { coerceFiniteNumber } from './safeFormat';
@@ -65,7 +66,12 @@ function walk(
seen.add(rowId);
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;
out.push({