N봉 기울기 판단로직 수정
This commit is contained in:
@@ -88,6 +88,7 @@ import {
|
||||
isValuePeriodFieldStored,
|
||||
parseThresholdField,
|
||||
resolveFieldCustomKind,
|
||||
resolveRightFieldCustomKind,
|
||||
getCompositeLeftCandleType,
|
||||
getCompositeRightCandleType,
|
||||
setCompositeLeftCandleType,
|
||||
@@ -143,9 +144,11 @@ interface HLThresh { over?: number; mid?: number; under?: number; }
|
||||
/** 하드코딩 기본값 — activeIndicators가 없을 때 폴백 */
|
||||
export const DEF_DEFAULTS = {
|
||||
rsiPeriod: 9,
|
||||
rsiSignal: 14,
|
||||
macdFast: 12, macdSlow: 26, macdSignal: 9,
|
||||
cciPeriod: 13,
|
||||
stochK: 12, stochD: 5,
|
||||
cciSignal: 10,
|
||||
stochK: 12, stochSmooth: 3, stochD: 5,
|
||||
adxPeriod: 14,
|
||||
trixPeriod: 12, trixSignal: 9,
|
||||
dmiPeriod: 14,
|
||||
@@ -285,11 +288,14 @@ export function buildDef(activeIndicators: IndicatorConfig[]): DefType {
|
||||
|
||||
return {
|
||||
rsiPeriod: num(rsi, 'length', DEF_DEFAULTS.rsiPeriod),
|
||||
rsiSignal: num(rsi, 'maLength', DEF_DEFAULTS.rsiSignal),
|
||||
macdFast: num(macd, 'fastLength', DEF_DEFAULTS.macdFast),
|
||||
macdSlow: num(macd, 'slowLength', DEF_DEFAULTS.macdSlow),
|
||||
macdSignal: num(macd, 'signalLength', DEF_DEFAULTS.macdSignal),
|
||||
cciPeriod: num(cci, 'length', DEF_DEFAULTS.cciPeriod),
|
||||
cciSignal: num(cci, 'maLength', DEF_DEFAULTS.cciSignal),
|
||||
stochK: num(stoch, 'kLength', DEF_DEFAULTS.stochK),
|
||||
stochSmooth: num(stoch, 'smooth', DEF_DEFAULTS.stochSmooth),
|
||||
stochD: num(stoch, 'dSmoothing', DEF_DEFAULTS.stochD),
|
||||
adxPeriod: num(adx, 'adxSmoothing', DEF_DEFAULTS.adxPeriod),
|
||||
trixPeriod: num(trix, 'length', DEF_DEFAULTS.trixPeriod),
|
||||
@@ -431,18 +437,44 @@ const ICHIMOKU_CONDS = [
|
||||
type Opt = { value: string; label: string };
|
||||
const NONE: Opt = { value: 'NONE', label: '선택안함' };
|
||||
|
||||
export const getFieldOpts = (
|
||||
export type FieldOptSlot = 'left' | 'right';
|
||||
|
||||
function mergeFieldOpts(...lists: Opt[][]): Opt[] {
|
||||
const seen = new Set<string>();
|
||||
const out: Opt[] = [];
|
||||
for (const list of lists) {
|
||||
for (const o of list) {
|
||||
if (seen.has(o.value)) continue;
|
||||
seen.add(o.value);
|
||||
out.push(o);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildThresholdOpts(
|
||||
th: { over: number; mid: number; under: number },
|
||||
overTerm: string,
|
||||
underTerm: string,
|
||||
includeMid = true,
|
||||
): Opt[] {
|
||||
const opts: Opt[] = [{ value: THRESHOLD_HL_OVER, label: `${overTerm}(${th.over})` }];
|
||||
if (includeMid) opts.push({ value: THRESHOLD_HL_MID, label: `중앙선(${th.mid})` });
|
||||
opts.push({ value: THRESHOLD_HL_UNDER, label: `${underTerm}(${th.under})` });
|
||||
return opts;
|
||||
}
|
||||
|
||||
function buildFieldOptsForSlot(
|
||||
ind: string,
|
||||
signalType: 'buy'|'sell',
|
||||
signalType: 'buy' | 'sell',
|
||||
DEF: DefType,
|
||||
cond?: ConditionDSL,
|
||||
): Opt[] => {
|
||||
cond: ConditionDSL | undefined,
|
||||
slot: FieldOptSlot,
|
||||
): Opt[] {
|
||||
const overTerm = signalType === 'buy' ? '과열진입' : '과열이탈';
|
||||
const underTerm = signalType === 'buy' ? '침체이탈' : '침체진입';
|
||||
const condOpts = (conds: Opt[]): Opt[] => [NONE, ...conds];
|
||||
|
||||
/** 해당 지표의 hline 임계값 — 없으면 기본값 */
|
||||
const th = (defaults: HLThresh): Required<HLThresh> => {
|
||||
const th = (defaults: { over?: number; mid?: number; under?: number }): { over: number; mid: number; under: number } => {
|
||||
const saved = DEF.hlThresh[ind] ?? {};
|
||||
return {
|
||||
over: saved.over ?? defaults.over ?? 100,
|
||||
@@ -451,77 +483,137 @@ export const getFieldOpts = (
|
||||
};
|
||||
};
|
||||
|
||||
switch(ind) {
|
||||
switch (ind) {
|
||||
case 'MACD': {
|
||||
const { mid } = th({ mid: 0 });
|
||||
if (slot === 'left') {
|
||||
return [
|
||||
{ value: 'MACD_LINE', label: 'MACD 선' },
|
||||
{ value: 'SIGNAL_LINE', label: `신호선 ${DEF.macdSignal}` },
|
||||
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: 'SIGNAL_LINE', label: `신호선 ${DEF.macdSignal}` },
|
||||
NONE,
|
||||
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
||||
];
|
||||
}
|
||||
case 'RSI': {
|
||||
const { over, mid, under } = th({ over:70, mid:50, under:30 });
|
||||
const rsiP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'RSI' }, DEF) : DEF.rsiPeriod;
|
||||
return condOpts([
|
||||
{ value:'RSI_VALUE', label:`RSI 라인(${rsiP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 70, mid: 50, under: 30 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [
|
||||
{ value: 'RSI_VALUE', label: `RSI 기간 ${rsiP}` },
|
||||
{ value: 'RSI_SIGNAL', label: `신호선 ${DEF.rsiSignal}` },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: 'RSI_SIGNAL', label: `신호선 ${DEF.rsiSignal}` },
|
||||
NONE,
|
||||
...thresholds,
|
||||
];
|
||||
}
|
||||
case 'STOCHASTIC': {
|
||||
const { over, mid, under } = th({ over:80, mid:50, under:20 });
|
||||
return condOpts([
|
||||
{ value:'STOCH_K', label:`Stochastic %K(${DEF.stochK}일)` },
|
||||
{ value:'STOCH_D', label:`Stochastic %D(${DEF.stochD}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 80, mid: 50, under: 20 }), overTerm, underTerm);
|
||||
const kLabel = `Stochastic 기간${DEF.stochK}일 %K${DEF.stochSmooth}일`;
|
||||
const dLabel = `Stochastic %D${DEF.stochD}`;
|
||||
if (slot === 'left') {
|
||||
return [
|
||||
{ value: 'STOCH_K', label: kLabel },
|
||||
{ value: 'STOCH_D', label: dLabel },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: 'STOCH_D', label: `${dLabel}일` },
|
||||
NONE,
|
||||
...thresholds,
|
||||
];
|
||||
}
|
||||
case 'CCI': {
|
||||
const { over, mid, under } = th({ over:100, mid:0, under:-100 });
|
||||
const cciP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'CCI' }, DEF) : DEF.cciPeriod;
|
||||
return condOpts([
|
||||
{ value:'CCI_VALUE', label:`CCI 라인(${cciP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 100, mid: 0, under: -100 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [
|
||||
{ value: 'CCI_VALUE', label: `CCI 기간 ${cciP}` },
|
||||
{ value: 'CCI_SIGNAL', label: `신호선 ${DEF.cciSignal}` },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: 'CCI_SIGNAL', label: `신호선 ${DEF.cciSignal}` },
|
||||
NONE,
|
||||
...thresholds,
|
||||
];
|
||||
}
|
||||
case 'ADX': {
|
||||
const { over, mid, under } = th({ over: 40, mid: 25, under: 20 });
|
||||
const adxP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'ADX' }, DEF) : DEF.adxPeriod;
|
||||
return condOpts([
|
||||
{ value:'ADX_VALUE', label:`ADX 라인(${adxP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 40, mid: 25, under: 20 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [{ value: 'ADX_VALUE', label: `ADX 기간 ${adxP}` }];
|
||||
}
|
||||
return [NONE, ...thresholds];
|
||||
}
|
||||
case 'TRIX': {
|
||||
const { mid } = th({ mid:0 });
|
||||
const trixP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'TRIX' }, DEF) : DEF.trixPeriod;
|
||||
return condOpts([
|
||||
{ value:'TRIX_VALUE', label:`TRIX 라인(${trixP}일)` },
|
||||
{ value:'TRIX_SIGNAL', label:`TRIX 시그널(${DEF.trixSignal}일)` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
]);
|
||||
const { mid } = th({ mid: 0 });
|
||||
if (slot === 'left') {
|
||||
return [
|
||||
{ value: 'TRIX_VALUE', label: `TRIX ${trixP}` },
|
||||
{ value: 'TRIX_SIGNAL', label: `TRMA ${DEF.trixSignal}` },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: 'TRIX_SIGNAL', label: `TRMA ${DEF.trixSignal}` },
|
||||
NONE,
|
||||
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
||||
];
|
||||
}
|
||||
case 'DMI': {
|
||||
const { over, mid, under } = th({ over:30, mid:20, under:10 });
|
||||
return condOpts([
|
||||
{ value:'PDI', label:`DI+(${DEF.dmiPeriod}일)` },
|
||||
{ value:'MDI', label:`DI-(${DEF.dmiPeriod}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`과열(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중간값(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`침체(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 30, mid: 20, under: 10 }), '과열', '침체');
|
||||
const plusLabel = `+DI 기간${DEF.dmiPeriod}일`;
|
||||
const minusLabel = `-DI 기간${DEF.dmiPeriod}일`;
|
||||
if (slot === 'left') {
|
||||
return [
|
||||
{ value: 'PDI', label: plusLabel },
|
||||
{ value: 'MDI', label: minusLabel },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: 'MDI', label: minusLabel },
|
||||
{ value: 'PDI', label: plusLabel },
|
||||
NONE,
|
||||
...thresholds,
|
||||
];
|
||||
}
|
||||
case 'OBV': return condOpts([
|
||||
{ value:'OBV_LINE', label:`OBV선(${DEF.obvPeriod}일)` },
|
||||
{ value:'OBV_SIGNAL', label:`신호선(${DEF.obvSignal}일)` },
|
||||
]);
|
||||
case 'VOLUME': return condOpts([
|
||||
{ value:'VOLUME_VALUE', label:'거래량' },
|
||||
{ value:'VOLUME_MA', label:'거래량 이동평균' },
|
||||
]);
|
||||
case 'OBV':
|
||||
if (slot === 'left') {
|
||||
return [
|
||||
{ value: 'OBV_LINE', label: 'OBV선' },
|
||||
{ value: 'OBV_SIGNAL', label: `신호선 ${DEF.obvSignal}일` },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ value: 'OBV_SIGNAL', label: `신호선 ${DEF.obvSignal}일` },
|
||||
NONE,
|
||||
];
|
||||
case 'WILLIAMS_R': {
|
||||
const wrP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'WILLIAMS_R' }, DEF) : DEF.williamsR;
|
||||
const thresholds = buildThresholdOpts(th({ over: -20, mid: -50, under: -80 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [{ value: 'WILLIAMS_R_VALUE', label: `williams%R 기간${wrP}` }];
|
||||
}
|
||||
return [NONE, ...thresholds];
|
||||
}
|
||||
case 'VOLUME':
|
||||
return slot === 'left'
|
||||
? [
|
||||
{ value: 'VOLUME_VALUE', label: '거래량' },
|
||||
{ value: 'VOLUME_MA', label: '거래량 이동평균' },
|
||||
]
|
||||
: [NONE, { value: 'VOLUME_MA', label: '거래량 이동평균' }];
|
||||
case 'MA': {
|
||||
let opts = [
|
||||
NONE,
|
||||
...(slot === 'right' ? [NONE] : []),
|
||||
...buildMaFieldOptions(DEF.maLines),
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
];
|
||||
@@ -531,7 +623,7 @@ export const getFieldOpts = (
|
||||
}
|
||||
case 'EMA': {
|
||||
let opts = [
|
||||
NONE,
|
||||
...(slot === 'right' ? [NONE] : []),
|
||||
...buildEmaFieldOptions(DEF.maLines, 4),
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
];
|
||||
@@ -541,159 +633,175 @@ export const getFieldOpts = (
|
||||
}
|
||||
case 'DISPARITY': {
|
||||
const { mid } = th({ mid: 100 });
|
||||
return condOpts([
|
||||
{ value:`DISPARITY${DEF.dispUltra}`, label:`초단기 이격도(${DEF.dispUltra}일)` },
|
||||
{ value:`DISPARITY${DEF.dispShort}`, label:`단기 이격도(${DEF.dispShort}일)` },
|
||||
{ value:`DISPARITY${DEF.dispMid}`, label:`중기 이격도(${DEF.dispMid}일)` },
|
||||
{ value:`DISPARITY${DEF.dispLong}`, label:`장기 이격도(${DEF.dispLong}일)` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
]);
|
||||
const lines = [
|
||||
{ value: `DISPARITY${DEF.dispUltra}`, label: `초단기 이격도(${DEF.dispUltra}일)` },
|
||||
{ value: `DISPARITY${DEF.dispShort}`, label: `단기 이격도(${DEF.dispShort}일)` },
|
||||
{ value: `DISPARITY${DEF.dispMid}`, label: `중기 이격도(${DEF.dispMid}일)` },
|
||||
{ value: `DISPARITY${DEF.dispLong}`, label: `장기 이격도(${DEF.dispLong}일)` },
|
||||
{ value: THRESHOLD_HL_MID, label: `중앙선(${mid})` },
|
||||
];
|
||||
return slot === 'right' ? [NONE, ...lines] : lines;
|
||||
}
|
||||
case 'PSYCHOLOGICAL': {
|
||||
const { over, mid, under } = th({ over:75, mid:50, under:25 });
|
||||
const psyP = cond
|
||||
? getConditionValuePeriod({ ...cond, indicatorType: ind }, DEF)
|
||||
: DEF.psyPeriod;
|
||||
return condOpts([
|
||||
{ value:'PSY_VALUE', label:`심리도 라인(${psyP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 75, mid: 50, under: 25 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [{ value: 'PSY_VALUE', label: `심리도 기간 ${psyP}` }];
|
||||
}
|
||||
return [NONE, ...thresholds];
|
||||
}
|
||||
case 'NEW_PSYCHOLOGICAL': {
|
||||
const { over, mid, under } = th({ over:50, mid:0, under:-50 });
|
||||
const nPsyP = cond
|
||||
? getConditionValuePeriod({ ...cond, indicatorType: ind }, DEF)
|
||||
: DEF.newPsy;
|
||||
return condOpts([
|
||||
{ value:'NEW_PSY_VALUE', label:`신심리도 라인(${nPsyP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 50, mid: 0, under: -50 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [{ value: 'NEW_PSY_VALUE', label: `신심리도 기간 ${nPsyP}` }];
|
||||
}
|
||||
return [NONE, ...thresholds];
|
||||
}
|
||||
case 'INVEST_PSYCHOLOGICAL': {
|
||||
const { over, mid, under } = th({ over:75, mid:50, under:25 });
|
||||
const ipsyP = cond
|
||||
? getConditionValuePeriod({ ...cond, indicatorType: 'INVEST_PSYCHOLOGICAL' }, DEF)
|
||||
: DEF.investPsy;
|
||||
return condOpts([
|
||||
{ value:'INVEST_PSY_VALUE', label:`투자심리도 라인(${ipsyP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
}
|
||||
case 'WILLIAMS_R': {
|
||||
const { over, mid, under } = th({ over:-20, mid:-50, under:-80 });
|
||||
const wrP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'WILLIAMS_R' }, DEF) : DEF.williamsR;
|
||||
return condOpts([
|
||||
{ value:'WILLIAMS_R_VALUE', label:`Williams %R 라인(${wrP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 75, mid: 50, under: 25 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [{ value: 'INVEST_PSY_VALUE', label: `투자심리도 기간 ${ipsyP}` }];
|
||||
}
|
||||
return [NONE, ...thresholds];
|
||||
}
|
||||
case 'BWI': {
|
||||
const { over, mid, under } = th({ over:80, mid:50, under:20 });
|
||||
const bwiP = DEF.bwiPeriod;
|
||||
return condOpts([
|
||||
{ value:'BWI_VALUE', label:`BWI 라인(${bwiP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 80, mid: 50, under: 20 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [{ value: 'BWI_VALUE', label: `BWI 기간 ${DEF.bwiPeriod}` }];
|
||||
}
|
||||
return [NONE, ...thresholds];
|
||||
}
|
||||
case 'VR': {
|
||||
const { over, mid, under } = th({ over:200, mid:100, under:50 });
|
||||
const vrP = cond ? getConditionValuePeriod({ ...cond, indicatorType: 'VR' }, DEF) : DEF.vrPeriod;
|
||||
return condOpts([
|
||||
{ value:'VR_VALUE', label:`VR 라인(${vrP}일)` },
|
||||
{ value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
{ value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` },
|
||||
]);
|
||||
const thresholds = buildThresholdOpts(th({ over: 200, mid: 100, under: 50 }), overTerm, underTerm);
|
||||
if (slot === 'left') {
|
||||
return [{ value: 'VR_VALUE', label: `VR 기간 ${vrP}` }];
|
||||
}
|
||||
return [NONE, ...thresholds];
|
||||
}
|
||||
case 'VOLUME_OSC': {
|
||||
const { mid } = th({ mid:0 });
|
||||
return condOpts([
|
||||
{ value:'VOLUME_OSC_VALUE', label:`거래량 오실레이터 값(${DEF.volOscShort}일/${DEF.volOscLong}일)` },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
]);
|
||||
const { mid } = th({ mid: 0 });
|
||||
if (slot === 'left') {
|
||||
return [{ value: 'VOLUME_OSC_VALUE', label: `거래량 오실레이터(${DEF.volOscShort}일/${DEF.volOscLong}일)` }];
|
||||
}
|
||||
return [NONE, { value: THRESHOLD_HL_MID, label: `중앙선(${mid})` }];
|
||||
}
|
||||
case 'MACD': {
|
||||
const { mid } = th({ mid:0 });
|
||||
return condOpts([
|
||||
{ value:'MACD_LINE', label:`MACD 라인(${DEF.macdFast}일/${DEF.macdSlow}일)` },
|
||||
{ value:'SIGNAL_LINE', label:`시그널 라인(${DEF.macdSignal}일)` },
|
||||
{ value:'HISTOGRAM', label:'히스토그램' },
|
||||
{ value:THRESHOLD_HL_MID, label:`중앙선(${mid})` },
|
||||
]);
|
||||
}
|
||||
case 'BOLLINGER': return condOpts([
|
||||
{ value:'UPPER_BAND', label:`상단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
|
||||
{ value:'MIDDLE_BAND', label:`중심선(${DEF.bbPeriod}일)` },
|
||||
{ value:'LOWER_BAND', label:`하단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
|
||||
{ value:'CLOSE_PRICE', label:'종가' },
|
||||
{ value:'OPEN_PRICE', label:'시가' },
|
||||
{ value:'HIGH_PRICE', label:'고가' },
|
||||
{ value:'LOW_PRICE', label:'저가' },
|
||||
]);
|
||||
case 'BOLLINGER':
|
||||
return slot === 'right'
|
||||
? [
|
||||
NONE,
|
||||
{ value: 'UPPER_BAND', label: `상단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
|
||||
{ value: 'MIDDLE_BAND', label: `중심선(${DEF.bbPeriod}일)` },
|
||||
{ value: 'LOWER_BAND', label: `하단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
{ value: 'OPEN_PRICE', label: '시가' },
|
||||
{ value: 'HIGH_PRICE', label: '고가' },
|
||||
{ value: 'LOW_PRICE', label: '저가' },
|
||||
]
|
||||
: [
|
||||
{ value: 'UPPER_BAND', label: `상단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
|
||||
{ value: 'MIDDLE_BAND', label: `중심선(${DEF.bbPeriod}일)` },
|
||||
{ value: 'LOWER_BAND', label: `하단밴드(${DEF.bbPeriod}일, σ${DEF.bbStdDev})` },
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
{ value: 'OPEN_PRICE', label: '시가' },
|
||||
{ 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([
|
||||
const lines = [
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
{ value: 'HIGH_PRICE', label: '고가' },
|
||||
{ value: 'OPEN_PRICE', label: '시가' },
|
||||
...periods.map(n => ({ value: nhPriorField(n), label: `${n}일 신고가선(직전 N봉)` })),
|
||||
]);
|
||||
];
|
||||
return slot === 'right' ? [NONE, ...lines] : lines;
|
||||
}
|
||||
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([
|
||||
const lines = [
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
{ value: 'LOW_PRICE', label: '저가' },
|
||||
{ value: 'OPEN_PRICE', label: '시가' },
|
||||
...periods.map(n => ({ value: nlPriorField(n), label: `${n}일 신저가선(직전 N봉)` })),
|
||||
]);
|
||||
];
|
||||
return slot === 'right' ? [NONE, ...lines] : lines;
|
||||
}
|
||||
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_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_55', label:'하단(55일 최저가)' },
|
||||
{ value:'CLOSE_PRICE', label:'종가' },
|
||||
{ value:'LOW_PRICE', label:'저가' },
|
||||
{ value:'HIGH_PRICE', label:'고가' },
|
||||
]);
|
||||
case 'ICHIMOKU': return condOpts([
|
||||
{ value:'CONVERSION_LINE', label:`전환선(${DEF.ichTenkan}일)` },
|
||||
{ value:'BASE_LINE', label:`기준선(${DEF.ichKijun}일)` },
|
||||
{ value:'LEADING_SPAN1', label:'선행스팬1' },
|
||||
{ value:'LEADING_SPAN2', label:`선행스팬2(${DEF.ichSenkouB}일)` },
|
||||
{ value:'LAGGING_SPAN', label:'후행스팬' },
|
||||
{ value:'CLOSE_PRICE', label:'종가' },
|
||||
{ value:'OPEN_PRICE', label:'시가' },
|
||||
{ value:'HIGH_PRICE', label:'고가' },
|
||||
{ value:'LOW_PRICE', label:'저가' },
|
||||
]);
|
||||
default: return [NONE];
|
||||
case 'DONCHIAN': {
|
||||
const lines = [
|
||||
{ value: 'DC_UPPER_9', label: '상단(9일 최고가·신고가)' },
|
||||
{ value: 'DC_UPPER_10', label: '상단(10일 최고가)' },
|
||||
{ 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_55', label: '하단(55일 최저가)' },
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
{ value: 'LOW_PRICE', label: '저가' },
|
||||
{ value: 'HIGH_PRICE', label: '고가' },
|
||||
];
|
||||
return slot === 'right' ? [NONE, ...lines] : lines;
|
||||
}
|
||||
case 'ICHIMOKU': {
|
||||
const lines = [
|
||||
{ value: 'CONVERSION_LINE', label: `전환선(${DEF.ichTenkan}일)` },
|
||||
{ value: 'BASE_LINE', label: `기준선(${DEF.ichKijun}일)` },
|
||||
{ value: 'LEADING_SPAN1', label: '선행스팬1' },
|
||||
{ value: 'LEADING_SPAN2', label: `선행스팬2(${DEF.ichSenkouB}일)` },
|
||||
{ value: 'LAGGING_SPAN', label: '후행스팬' },
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
{ value: 'OPEN_PRICE', label: '시가' },
|
||||
{ value: 'HIGH_PRICE', label: '고가' },
|
||||
{ value: 'LOW_PRICE', label: '저가' },
|
||||
];
|
||||
return slot === 'right' ? [NONE, ...lines] : lines;
|
||||
}
|
||||
default:
|
||||
return slot === 'right' ? [NONE] : [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getLeftFieldOpts = (
|
||||
ind: string,
|
||||
signalType: 'buy' | 'sell',
|
||||
DEF: DefType,
|
||||
cond?: ConditionDSL,
|
||||
): Opt[] => buildFieldOptsForSlot(ind, signalType, DEF, cond, 'left');
|
||||
|
||||
export const getRightFieldOpts = (
|
||||
ind: string,
|
||||
signalType: 'buy' | 'sell',
|
||||
DEF: DefType,
|
||||
cond?: ConditionDSL,
|
||||
): Opt[] => buildFieldOptsForSlot(ind, signalType, DEF, cond, 'right');
|
||||
|
||||
export const getFieldOpts = (
|
||||
ind: string,
|
||||
signalType: 'buy'|'sell',
|
||||
DEF: DefType,
|
||||
cond?: ConditionDSL,
|
||||
): Opt[] => mergeFieldOpts(
|
||||
getLeftFieldOpts(ind, signalType, DEF, cond),
|
||||
getRightFieldOpts(ind, signalType, DEF, cond),
|
||||
);
|
||||
|
||||
const getDefaultConditionFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
|
||||
const map: Record<string, {l:string;r:string}> = {
|
||||
RSI: { l:'RSI_VALUE', r: THRESHOLD_HL_OVER },
|
||||
STOCHASTIC: { l:'STOCH_K', r:'STOCH_D' },
|
||||
CCI: { l:'CCI_VALUE', r: THRESHOLD_HL_OVER },
|
||||
CCI: { l:'CCI_VALUE', r:'CCI_SIGNAL' },
|
||||
ADX: { l:'ADX_VALUE', r: THRESHOLD_HL_MID },
|
||||
TRIX: { l:'TRIX_VALUE', r:'TRIX_SIGNAL' },
|
||||
DMI: { l:'PDI', r:'MDI' },
|
||||
@@ -968,7 +1076,9 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
||||
cond, signalType, onChange, def, stochPairEdit,
|
||||
}) => {
|
||||
const normalized = normalizeCompositeCondition(cond);
|
||||
const fieldOpts = getFieldOpts(normalized.indicatorType, signalType, def, normalized);
|
||||
const leftFieldOpts = getLeftFieldOpts(normalized.indicatorType, signalType, def, normalized);
|
||||
const rightFieldOpts = getRightFieldOpts(normalized.indicatorType, signalType, def, normalized);
|
||||
const fieldOpts = mergeFieldOpts(leftFieldOpts, rightFieldOpts);
|
||||
const condOpts = getCondOptionsForIndicator(normalized.indicatorType);
|
||||
|
||||
const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
|
||||
@@ -1220,11 +1330,11 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
||||
<div className="sp-cond-field">
|
||||
<label className="sp-cond-lbl">조건대상1</label>
|
||||
<ComboFieldSelect
|
||||
options={fieldOpts}
|
||||
options={leftFieldOpts}
|
||||
fieldValue={isHoldType ? 'NONE' : (normalized.leftField ?? getLeftValue())}
|
||||
isPresetField={!isHoldType && (
|
||||
fieldOpts.some(o => o.value === normalized.leftField)
|
||||
|| (fieldOpts.some(o => o.value === getLeftValue())
|
||||
leftFieldOpts.some(o => o.value === normalized.leftField)
|
||||
|| (leftFieldOpts.some(o => o.value === getLeftValue())
|
||||
&& !isValuePeriodFieldStored(normalized.indicatorType, normalized.leftField))
|
||||
)}
|
||||
customKind={resolveFieldCustomKind(normalized.indicatorType, normalized.leftField)}
|
||||
@@ -1260,10 +1370,14 @@ export const CondEditor: React.FC<CondEditorProps> = ({
|
||||
<div className="sp-cond-field">
|
||||
<label className="sp-cond-lbl">조건대상2</label>
|
||||
<ComboFieldSelect
|
||||
options={fieldOpts}
|
||||
options={rightFieldOpts}
|
||||
fieldValue={rightValue}
|
||||
isPresetField={fieldOpts.some(o => o.value === normalized.rightField)}
|
||||
customKind={resolveFieldCustomKind(normalized.indicatorType, normalized.rightField)}
|
||||
isPresetField={
|
||||
!isThresholdOverridden(normalized)
|
||||
&& parseThresholdField(normalized.rightField) == null
|
||||
&& rightFieldOpts.some(o => o.value === getRightValue())
|
||||
}
|
||||
customKind={resolveRightFieldCustomKind(normalized.indicatorType, normalized.rightField)}
|
||||
customNumber={getConditionThreshold(normalized, 'right', inheritedThresholdField, def)
|
||||
?? getChartReferenceThreshold(normalized, 'right', inheritedThresholdField, def)
|
||||
?? parseThresholdField(normalized.rightField)
|
||||
|
||||
Reference in New Issue
Block a user