가상투자 종목카드박스 레이아웃 수정

This commit is contained in:
Macbook
2026-05-26 16:42:57 +09:00
parent 7cf43609dc
commit c0d21ac825
25 changed files with 1632 additions and 61 deletions
+54 -6
View File
@@ -9,9 +9,17 @@ import {
compositeFieldLabel,
makeCompositeCondition,
normalizeCompositeCondition,
parseDonchianPeriod,
parsePeriodFromCompositeField,
syncCompositeFields,
} from '../utils/compositeIndicators';
import {
isPriceExtremeIndicator,
nhPriorField,
nlPriorField,
parsePriorExtremePeriod,
syncPriceExtremeSimpleFields,
} from '../utils/priceExtremeIndicators';
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
import {
getCompositeFieldOpts,
@@ -177,6 +185,8 @@ export function buildDef(activeIndicators: IndicatorConfig[]): DefType {
OBV: 'OBV',
BOLLINGER: 'BollingerBands',
DONCHIAN: 'DonchianChannels',
NEW_HIGH: 'PriceExtreme',
NEW_LOW: 'PriceExtreme',
};
const extractThresh = (dslType: string): HLThresh => {
@@ -469,13 +479,35 @@ export const getFieldOpts = (
{ value:'HIGH_PRICE', label:'고가' },
{ value:'LOW_PRICE', label:'저가' },
]);
case 'NEW_HIGH': {
const p = cond?.period ?? 9;
const periods = [...new Set([p, 5, 9, 10, 20, 55])].sort((a, b) => a - b);
return condOpts([
{ value: 'CLOSE_PRICE', label: '종가' },
{ value: 'HIGH_PRICE', label: '고가' },
{ value: 'OPEN_PRICE', label: '시가' },
...periods.map(n => ({ value: nhPriorField(n), label: `${n}일 신고가선(직전 N봉)` })),
]);
}
case 'NEW_LOW': {
const p = cond?.period ?? 9;
const periods = [...new Set([p, 5, 9, 10, 20, 55])].sort((a, b) => a - b);
return condOpts([
{ value: 'CLOSE_PRICE', label: '종가' },
{ value: 'LOW_PRICE', label: '저가' },
{ value: 'OPEN_PRICE', label: '시가' },
...periods.map(n => ({ value: nlPriorField(n), label: `${n}일 신저가선(직전 N봉)` })),
]);
}
case 'DONCHIAN': return condOpts([
{ value:'DC_UPPER_9', label:'상단(9일 최고가·신고가)' },
{ value:'DC_UPPER_10', label:'상단(10일 최고가)' },
{ value:'DC_UPPER_20', label:'상단(20일 최고가)' },
{ value:'DC_UPPER_20', label:'상단(20일 최고가·신고가)' },
{ value:'DC_UPPER_55', label:'상단(55일 최고가)' },
{ value:'DC_MIDDLE_20', label:'중심(20일)' },
{ value:'DC_LOWER_9', label:'하단(9일 최저가·신저가)' },
{ value:'DC_LOWER_10', label:'하단(10일 최저가)' },
{ value:'DC_LOWER_20', label:'하단(20일 최저가)' },
{ value:'DC_LOWER_20', label:'하단(20일 최저가·신저가)' },
{ value:'DC_LOWER_55', label:'하단(55일 최저가)' },
{ value:'CLOSE_PRICE', label:'종가' },
{ value:'LOW_PRICE', label:'저가' },
@@ -526,6 +558,8 @@ const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType):
DONCHIAN: signalType === 'buy'
? { l:'CLOSE_PRICE', r:'DC_UPPER_20' }
: { l:'CLOSE_PRICE', r:'DC_LOWER_20' },
NEW_HIGH: { l:'CLOSE_PRICE', r: nhPriorField(9) },
NEW_LOW: { l:'CLOSE_PRICE', r: nlPriorField(9) },
ICHIMOKU: { l:'CONVERSION_LINE', r:'BASE_LINE' },
};
return map[ind] ?? { l:'NONE', r:'NONE' };
@@ -569,6 +603,9 @@ const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: Def
updated.leftField = 'LAGGING_SPAN'; updated.rightField = 'CLOSE_PRICE';
}
}
if (isPriceExtremeIndicator(updated.indicatorType)) {
return syncPriceExtremeSimpleFields(updated);
}
return updated;
};
@@ -751,8 +788,12 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
OVERBOUGHT_200: 200, NEUTRAL_100: 100, OVERSOLD_50: 50,
ZERO_LINE: 0,
};
const upd: ConditionDSL = { ...normalized, rightField: v };
let upd: ConditionDSL = { ...normalized, rightField: v };
if (thresholds[v] !== undefined) upd.targetValue = thresholds[v];
const priorP = parsePriorExtremePeriod(v);
if (priorP != null && isPriceExtremeIndicator(normalized.indicatorType)) {
upd = syncPriceExtremeSimpleFields({ ...upd, period: priorP });
}
onChange(upd);
};
@@ -769,7 +810,8 @@ export const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChan
const periodPresetsRight = getCompositePeriodPresetOptions(normalized.indicatorType, def, 'right');
const handleCompositeField = (side: 'left' | 'right', field: string) => {
const p = parsePeriodFromCompositeField(normalized.indicatorType, field);
const p = parseDonchianPeriod(field)
?? parsePeriodFromCompositeField(normalized.indicatorType, field);
if (p == null) return;
if (side === 'left') {
onChange(syncCompositeFields({ ...normalized, leftPeriod: p, leftField: field }));
@@ -1035,16 +1077,22 @@ export const makeNode = (
}
const def = getDefaultFields(value, signalType, DEF);
const period = options?.period ?? getDefaultIndicatorPeriod(value, DEF);
let conditionType = signalType === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN';
if (value === 'NEW_HIGH' && signalType === 'buy') conditionType = 'CROSS_UP';
if (value === 'NEW_LOW' && signalType === 'sell') conditionType = 'CROSS_DOWN';
const baseCondition: ConditionDSL = {
indicatorType: value,
conditionType: signalType === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN',
conditionType,
leftField: def.l,
rightField: def.r,
candleRange: 1,
period,
};
const condition = initConditionPeriods(value, baseCondition, DEF);
return {
id: genId(), type: 'CONDITION',
condition: initConditionPeriods(value, baseCondition, DEF),
condition: isPriceExtremeIndicator(value)
? syncPriceExtremeSimpleFields(condition)
: condition,
};
};