This commit is contained in:
Macbook
2026-06-18 00:56:50 +09:00
parent e5b5af093e
commit ac55748aec
7 changed files with 254 additions and 7 deletions
@@ -65,4 +65,22 @@ assertOk('directInput 55 => K_55', direct55.rightField === 'K_55' && direct55.ta
const filled = ensureDirectThresholdSnapshot({ indicatorType: 'RSI', rightField: 'K_55', thresholdOverride: false });
assertOk('K_55 fills targetValue', filled.targetValue === 55 && filled.thresholdOverride === true);
// 레거시 마이그레이션 — K_55 를 HL_MID(50) 로 접지 않음
function migrateK(cond) {
if (cond.thresholdOverride === true) return cond;
const field = cond.rightField;
if (!field?.startsWith('K_')) return cond;
const val = parseFloat(field.slice(2));
if (!Number.isFinite(val)) return cond;
const DEFAULT = { RSI: { over: 70, mid: 50, under: 30 } };
const th = DEFAULT.RSI;
const eps = 0.0001;
if (th.over != null && Math.abs(th.over - val) < eps) return { ...cond, rightField: 'HL_OVER', thresholdOverride: false };
if (th.mid != null && Math.abs(th.mid - val) < eps) return { ...cond, rightField: 'HL_MID', thresholdOverride: false };
if (th.under != null && Math.abs(th.under - val) < eps) return { ...cond, rightField: 'HL_UNDER', thresholdOverride: false };
return { ...cond, rightField: field, targetValue: val, thresholdOverride: true };
}
const migrated = migrateK({ indicatorType: 'RSI', rightField: 'K_55', thresholdOverride: false });
assertOk('migrate K_55 stays K_55', migrated.rightField === 'K_55' && migrated.targetValue === 55);
process.exit(failed > 0 ? 1 : 0);
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
export const COMBO_FIELD_CUSTOM = '__field_custom__';
@@ -117,6 +117,22 @@ export default function ComboFieldSelect({
setDraft(String(clamped));
}, [draft, allowDecimal, min, max, onCustomNumberChange, customNumber]);
const commitRef = useRef(commitCustom);
commitRef.current = commitCustom;
// 직접입력 blur 전 탭 전환·저장 시 값 유실 방지
useEffect(() => () => { commitRef.current(); }, []);
useEffect(() => {
if (!focused || mode !== 'custom') return;
const parsed = parseDraft(draft, allowDecimal);
if (parsed == null) return;
const clamped = clampValue(parsed, min, max, allowDecimal);
if (clamped === customNumber) return;
const t = window.setTimeout(() => onCustomNumberChange(clamped), 400);
return () => window.clearTimeout(t);
}, [draft, focused, mode, allowDecimal, min, max, customNumber, onCustomNumberChange]);
return (
<div className={`se-combo-field${mode === 'custom' ? ' se-combo-field--custom-only' : ''}`}>
<select
@@ -382,7 +382,7 @@ const StrategyEvaluationChart: React.FC<Props> = ({
}
void runSignalScanForBars(bars);
return () => { ++backtestGenRef.current; };
}, [strategy?.id, market, timeframe, paramsRevision, bars, loading, runSignalScanForBars]);
}, [strategy?.id, strategy?.buyCondition, market, timeframe, paramsRevision, bars, loading, runSignalScanForBars]);
useEffect(() => {
applyMarkers();
@@ -11,11 +11,11 @@ import {
} from './conditionPeriods';
import {
defaultInheritedThresholdField,
inferThresholdSymbolFromLegacy,
inferThresholdSymbolFromLegacyExact,
isThresholdFieldOrSymbol,
isThresholdSymbol,
} from './thresholdSymbols';
import { parseThresholdField, setDirectThreshold } from './conditionPeriods';
import { repairPriceExtremePairForest } from './priceExtremeBreakoutPair';
function isConditionCarrier(node: LogicNode): node is LogicNode & { condition: ConditionDSL } {
@@ -34,8 +34,16 @@ function migrateConditionThreshold(
let rightField = cond.rightField;
if (rightField?.startsWith('K_')) {
rightField = inferThresholdSymbolFromLegacyExact(rightField, cond.indicatorType, {})
?? inferThresholdSymbolFromLegacy(rightField, cond.indicatorType, {});
const exact = inferThresholdSymbolFromLegacyExact(rightField, cond.indicatorType, def.hlThresh);
if (exact) {
rightField = exact;
} else {
// K_55 등 비표준값 — 가장 가까운 HL_* 로 접지 않고 숫자 스냅샷 유지
const val = parseThresholdField(rightField);
if (val != null) {
return migrateConditionPeriod(setDirectThreshold(cond, 'right', val));
}
}
}
if (!rightField || (!isThresholdSymbol(rightField) && !rightField.startsWith('K_'))) {
const defaults = getDefaultConditionFields(cond.indicatorType, signalType, def);
@@ -1135,6 +1135,9 @@ export const CondEditor: React.FC<CondEditorProps> = ({
};
const getRightValue = () => {
const raw = normalized.rightField;
if (isThresholdOverridden(normalized) && raw?.startsWith('K_')) {
return raw;
}
const v = isThresholdOverridden(normalized)
? resolveFieldOptionValue(normalized.indicatorType, raw)
: (isThresholdFieldOrSymbol(raw)