전략 직접입력값 평가오류
This commit is contained in:
@@ -20,6 +20,11 @@ public final class IndicatorHlineResolver {
|
||||
|
||||
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);
|
||||
if (override) {
|
||||
if (cond.has("targetValue") && !cond.path("targetValue").isNull()) {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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,6 +302,7 @@ export function setConditionThreshold(
|
||||
? getChartReferenceThreshold(cond, side, inheritedField, def)
|
||||
: null;
|
||||
|
||||
if (!options?.directInput) {
|
||||
const matchedRole = def
|
||||
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
|
||||
: null;
|
||||
@@ -298,6 +318,7 @@ export function setConditionThreshold(
|
||||
const { targetValue: _t, ...rest } = cond;
|
||||
return { ...rest, [fieldKey]: role, thresholdOverride: false };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...cond,
|
||||
|
||||
@@ -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,
|
||||
{},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
{/* 조건 */}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user