전략 직접입력값 평가오류
This commit is contained in:
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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,20 +302,22 @@ export function setConditionThreshold(
|
|||||||
? getChartReferenceThreshold(cond, side, inheritedField, def)
|
? getChartReferenceThreshold(cond, side, inheritedField, def)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const matchedRole = def
|
if (!options?.directInput) {
|
||||||
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
|
const matchedRole = def
|
||||||
: null;
|
? inferThresholdRoleForValue(value, cond.indicatorType, def.hlThresh)
|
||||||
if (matchedRole != null) {
|
: null;
|
||||||
const { targetValue: _t, ...rest } = cond;
|
if (matchedRole != null) {
|
||||||
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
const { targetValue: _t, ...rest } = cond;
|
||||||
}
|
return { ...rest, [fieldKey]: matchedRole, thresholdOverride: false };
|
||||||
|
}
|
||||||
|
|
||||||
if (chartRef != null && Math.abs(chartRef - value) < 0.0001) {
|
if (chartRef != null && Math.abs(chartRef - value) < 0.0001) {
|
||||||
const role = isThresholdSymbol(inheritedField)
|
const role = isThresholdSymbol(inheritedField)
|
||||||
? inheritedField
|
? inheritedField
|
||||||
: (side === 'right' ? THRESHOLD_HL_OVER : inheritedField);
|
: (side === 'right' ? THRESHOLD_HL_OVER : inheritedField);
|
||||||
const { targetValue: _t, ...rest } = cond;
|
const { targetValue: _t, ...rest } = cond;
|
||||||
return { ...rest, [fieldKey]: role, thresholdOverride: false };
|
return { ...rest, [fieldKey]: role, thresholdOverride: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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,
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
{/* 조건 */}
|
{/* 조건 */}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user