N봉 기울기 판단로직 수정
This commit is contained in:
@@ -12,9 +12,7 @@ import {
|
||||
loadStrategies, saveStrategy, deleteStrategy,
|
||||
type StrategyDto as ApiStrategyDto,
|
||||
} from '../utils/backendApi';
|
||||
import { getIndicatorDef, getHLineLabel } from '../utils/indicatorRegistry';
|
||||
import {
|
||||
getStrategyTemplates,
|
||||
import { getStrategyTemplates,
|
||||
simpleTemplateToNode,
|
||||
type StrategyTemplateDef,
|
||||
} from '../utils/strategyPresets';
|
||||
@@ -29,6 +27,11 @@ import {
|
||||
buildEmaFieldOptions,
|
||||
buildMaFieldOptions,
|
||||
} from '../utils/maDslField';
|
||||
import {
|
||||
buildDef,
|
||||
DEF_DEFAULTS,
|
||||
type DefType,
|
||||
} from '../utils/strategyEditorShared';
|
||||
|
||||
// ─── 타입 ─────────────────────────────────────────────────────────────────────
|
||||
type LogicNodeType = 'AND' | 'OR' | 'NOT' | 'CONDITION' | 'TIMEFRAME';
|
||||
@@ -81,185 +84,10 @@ const genId = () => `n_${++_cnt}_${Date.now()}`;
|
||||
const loadStratsLocal = (): StrategyDto[] => [];
|
||||
const saveStratsLocal = (_s: StrategyDto[]) => { /* DB only */ };
|
||||
|
||||
// ─── 기본 파라미터 (IndicatorSettings 미사용 → 하드코딩 기본값) ─────────────
|
||||
// ─── 기본 파라미터 — strategyEditorShared.buildDef 사용 ─────────────────────
|
||||
/** 지표별 hline 임계값 (과열선/중앙선/침체선) */
|
||||
interface HLThresh { over?: number; mid?: number; under?: number; }
|
||||
|
||||
/** 하드코딩 기본값 — activeIndicators가 없을 때 폴백 */
|
||||
const DEF_DEFAULTS = {
|
||||
rsiPeriod: 9,
|
||||
macdFast: 12, macdSlow: 26, macdSignal: 9,
|
||||
cciPeriod: 13,
|
||||
stochK: 12, stochD: 5,
|
||||
adxPeriod: 14,
|
||||
trixPeriod: 12, trixSignal: 9,
|
||||
dmiPeriod: 14,
|
||||
obvPeriod: 9, obvSignal: 9,
|
||||
bbPeriod: 20, bbStdDev: 2,
|
||||
ichTenkan: 9, ichKijun: 26, ichSenkouB: 52,
|
||||
newPsy: 10, psyPeriod: 12, investPsy: 10,
|
||||
williamsR: 14,
|
||||
bwiPeriod: 20,
|
||||
vrPeriod: 10,
|
||||
volOscShort: 5, volOscLong: 10,
|
||||
dispUltra: 5, dispShort: 10, dispMid: 20, dispLong: 60,
|
||||
maLines: [5, 10, 20, 60, 120] as number[],
|
||||
/** 지표별 hline 임계값 (과열선/중앙선/침체선) */
|
||||
hlThresh: {
|
||||
RSI: { over: 70, mid: 50, under: 30 },
|
||||
STOCHASTIC: { over: 80, mid: 50, under: 20 },
|
||||
CCI: { over: 100, mid: 0, under: -100 },
|
||||
WILLIAMS_R: { over: -20, mid: -50, under: -80 },
|
||||
BWI: { over: 80, mid: 50, under: 20 },
|
||||
VR: { over: 200, mid: 100, under: 50 },
|
||||
PSYCHOLOGICAL: { over: 75, mid: 50, under: 25 },
|
||||
NEW_PSYCHOLOGICAL: { over: 50, mid: 0, under: -50 },
|
||||
INVEST_PSYCHOLOGICAL: { over: 75, mid: 50, under: 25 },
|
||||
MACD: { mid: 0 },
|
||||
ADX: { over: 40, mid: 25, under: 20 },
|
||||
DMI: { over: 30, mid: 20, under: 10 },
|
||||
TRIX: { mid: 0 },
|
||||
VOLUME_OSC: { mid: 0 },
|
||||
} as Record<string, HLThresh>,
|
||||
};
|
||||
type DefType = typeof DEF_DEFAULTS;
|
||||
|
||||
/**
|
||||
* activeIndicators에서 파라미터를 추출해 DEF를 구성.
|
||||
* 레지스트리 type명 / params 키는 indicatorRegistry.ts 의 defaultParams 기준.
|
||||
*/
|
||||
function buildDef(activeIndicators: IndicatorConfig[]): DefType {
|
||||
// 레지스트리 type명으로 조회
|
||||
const p = (registryType: string) =>
|
||||
activeIndicators.find(i => i.type === registryType)?.params ?? {};
|
||||
const num = (params: Record<string, number | string | boolean>, key: string, fallback: number) =>
|
||||
typeof params[key] === 'number' ? (params[key] as number) : fallback;
|
||||
|
||||
// ── 각 지표별 실제 레지스트리 type명 + params 키 ──────────────────────────
|
||||
// RSI → type:'RSI', params.length
|
||||
const rsi = p('RSI');
|
||||
// MACD → type:'MACD', params.fastLength / slowLength / signalLength
|
||||
const macd = p('MACD');
|
||||
// CCI → type:'CCI', params.length
|
||||
const cci = p('CCI');
|
||||
// Stochastic → type:'Stochastic', params.kLength / dSmoothing
|
||||
const stoch = p('Stochastic');
|
||||
// ADX → type:'ADX', params.adxSmoothing / diLength
|
||||
const adx = p('ADX');
|
||||
// TRIX → type:'TRIX', params.length / signalLength
|
||||
const trix = p('TRIX');
|
||||
// DMI → type:'DMI', params.diLength / adxSmoothing
|
||||
const dmi = p('DMI');
|
||||
// BollingerBands→ type:'BollingerBands', params.length / mult
|
||||
const bb = p('BollingerBands');
|
||||
// IchimokuCloud → type:'IchimokuCloud', params.conversionPeriods / basePeriods / laggingSpan2Periods
|
||||
const ich = p('IchimokuCloud');
|
||||
// Williams %R → type:'WilliamsPercentRange', params.length
|
||||
const wr = p('WilliamsPercentRange');
|
||||
const vo = p('VolumeOscillator');
|
||||
const vrInd = p('VR');
|
||||
const disp = p('Disparity');
|
||||
const psy = p('Psychological');
|
||||
const nPsy = p('NewPsychological');
|
||||
const iPsy = p('InvestPsychological');
|
||||
const obvP = p('OBV');
|
||||
// SMA (MA lines)→ type:'SMA', params keys depend on smaConfig
|
||||
const sma = p('SMA');
|
||||
|
||||
// MA 기간 배열: SMA 설정에서 period1~period11 추출 (smaConfig 구조)
|
||||
let maLines = DEF_DEFAULTS.maLines;
|
||||
const smaPeriods = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
.map(i => sma[`period${i}`])
|
||||
.filter(v => typeof v === 'number' && (v as number) > 0) as number[];
|
||||
if (smaPeriods.length >= 2) maLines = smaPeriods;
|
||||
|
||||
// ── hline 임계값 추출 ───────────────────────────────────────────────────────
|
||||
// DSL 지표타입 → 레지스트리 type명 매핑
|
||||
const DSL_TO_REGISTRY: Record<string, string> = {
|
||||
RSI: 'RSI',
|
||||
STOCHASTIC: 'Stochastic',
|
||||
CCI: 'CCI',
|
||||
WILLIAMS_R: 'WilliamsPercentRange',
|
||||
MACD: 'MACD',
|
||||
DMI: 'DMI',
|
||||
ADX: 'ADX',
|
||||
TRIX: 'TRIX',
|
||||
VOLUME_OSC: 'VolumeOscillator',
|
||||
VR: 'VR',
|
||||
DISPARITY: 'Disparity',
|
||||
PSYCHOLOGICAL: 'Psychological',
|
||||
NEW_PSYCHOLOGICAL: 'NewPsychological',
|
||||
INVEST_PSYCHOLOGICAL: 'InvestPsychological',
|
||||
OBV: 'OBV',
|
||||
BOLLINGER: 'BollingerBands',
|
||||
};
|
||||
|
||||
const extractThresh = (dslType: string): HLThresh => {
|
||||
const regType = DSL_TO_REGISTRY[dslType];
|
||||
if (!regType) return DEF_DEFAULTS.hlThresh[dslType] ?? {};
|
||||
const config = activeIndicators.find(i => i.type === regType);
|
||||
const defHl = getIndicatorDef(regType)?.hlines ?? [];
|
||||
const hlines = config?.hlines ?? defHl;
|
||||
if (hlines.length === 0) return DEF_DEFAULTS.hlThresh[dslType] ?? {};
|
||||
|
||||
const prices = hlines.map(h => h.price);
|
||||
const find = (lbl: string) => hlines.find(h =>
|
||||
(h.label ?? getHLineLabel(h.price, prices)) === lbl
|
||||
);
|
||||
const result: HLThresh = {};
|
||||
const ov = find('과열선'); if (ov) result.over = ov.price;
|
||||
const md = find('중앙선') ?? find('기준선'); if (md) result.mid = md.price;
|
||||
const un = find('침체선'); if (un) result.under = un.price;
|
||||
// 값이 하나도 없으면 기본값 사용
|
||||
if (result.over === undefined && result.mid === undefined && result.under === undefined)
|
||||
return DEF_DEFAULTS.hlThresh[dslType] ?? {};
|
||||
return {
|
||||
...DEF_DEFAULTS.hlThresh[dslType],
|
||||
...result,
|
||||
};
|
||||
};
|
||||
|
||||
const hlThresh: Record<string, HLThresh> = {};
|
||||
for (const dslType of Object.keys(DEF_DEFAULTS.hlThresh)) {
|
||||
hlThresh[dslType] = extractThresh(dslType);
|
||||
}
|
||||
|
||||
return {
|
||||
rsiPeriod: num(rsi, 'length', DEF_DEFAULTS.rsiPeriod),
|
||||
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),
|
||||
stochK: num(stoch, 'kLength', DEF_DEFAULTS.stochK),
|
||||
stochD: num(stoch, 'dSmoothing', DEF_DEFAULTS.stochD),
|
||||
adxPeriod: num(adx, 'adxSmoothing', DEF_DEFAULTS.adxPeriod),
|
||||
trixPeriod: num(trix, 'length', DEF_DEFAULTS.trixPeriod),
|
||||
trixSignal: num(trix, 'signalLength', DEF_DEFAULTS.trixSignal),
|
||||
dmiPeriod: num(dmi, 'diLength', num(dmi, 'length', DEF_DEFAULTS.dmiPeriod)),
|
||||
obvPeriod: num(obvP, 'maLength', DEF_DEFAULTS.obvPeriod),
|
||||
obvSignal: num(obvP, 'maLength', DEF_DEFAULTS.obvSignal),
|
||||
bbPeriod: num(bb, 'length', DEF_DEFAULTS.bbPeriod),
|
||||
bbStdDev: num(bb, 'mult', DEF_DEFAULTS.bbStdDev),
|
||||
ichTenkan: num(ich, 'conversionPeriods', DEF_DEFAULTS.ichTenkan),
|
||||
ichKijun: num(ich, 'basePeriods', DEF_DEFAULTS.ichKijun),
|
||||
ichSenkouB: num(ich, 'laggingSpan2Periods', DEF_DEFAULTS.ichSenkouB),
|
||||
psyPeriod: num(psy, 'length', DEF_DEFAULTS.psyPeriod),
|
||||
newPsy: num(nPsy, 'length', DEF_DEFAULTS.newPsy),
|
||||
investPsy: num(iPsy, 'length', DEF_DEFAULTS.investPsy),
|
||||
williamsR: num(wr, 'length', DEF_DEFAULTS.williamsR),
|
||||
bwiPeriod: DEF_DEFAULTS.bwiPeriod, // BWI는 레지스트리 미등록
|
||||
vrPeriod: num(vrInd, 'length', DEF_DEFAULTS.vrPeriod),
|
||||
volOscShort: num(vo, 'shortLength', DEF_DEFAULTS.volOscShort),
|
||||
volOscLong: num(vo, 'longLength', DEF_DEFAULTS.volOscLong),
|
||||
dispUltra: num(disp, 'ultraLength', DEF_DEFAULTS.dispUltra),
|
||||
dispShort: num(disp, 'shortLength', DEF_DEFAULTS.dispShort),
|
||||
dispMid: num(disp, 'midLength', DEF_DEFAULTS.dispMid),
|
||||
dispLong: num(disp, 'longLength', DEF_DEFAULTS.dispLong),
|
||||
maLines,
|
||||
hlThresh,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* hline 임계값을 K_ 접두어 DSL 필드값으로 변환.
|
||||
* 예: 70 → "K_70", -100 → "K_-100"
|
||||
|
||||
@@ -36,6 +36,11 @@ export const TopMenuBarNavGroups = memo(function TopMenuBarNavGroups({
|
||||
}: Props) {
|
||||
const [openGroupId, setOpenGroupId] = useState<string | null>(null);
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const hoverCapableRef = useRef(
|
||||
typeof window !== 'undefined'
|
||||
&& window.matchMedia('(hover: hover) and (pointer: fine)').matches,
|
||||
);
|
||||
|
||||
const itemByPage = useMemo(() => {
|
||||
const map = new Map<MenuPage, MenuItemDef>();
|
||||
@@ -54,6 +59,28 @@ export const TopMenuBarNavGroups = memo(function TopMenuBarNavGroups({
|
||||
|
||||
const closeDropdown = useCallback(() => setOpenGroupId(null), []);
|
||||
|
||||
const cancelScheduledClose = useCallback(() => {
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openGroup = useCallback((groupId: string) => {
|
||||
cancelScheduledClose();
|
||||
setOpenGroupId(groupId);
|
||||
}, [cancelScheduledClose]);
|
||||
|
||||
const scheduleClose = useCallback(() => {
|
||||
cancelScheduledClose();
|
||||
closeTimerRef.current = setTimeout(() => {
|
||||
closeTimerRef.current = null;
|
||||
setOpenGroupId(null);
|
||||
}, 120);
|
||||
}, [cancelScheduledClose]);
|
||||
|
||||
useEffect(() => () => cancelScheduledClose(), [cancelScheduledClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!openGroupId) return;
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
@@ -96,13 +123,22 @@ export const TopMenuBarNavGroups = memo(function TopMenuBarNavGroups({
|
||||
return (
|
||||
<React.Fragment key={group.id}>
|
||||
{index > 0 && <span className="tmb-divider tmb-divider--nav" aria-hidden="true" />}
|
||||
<div className={`tmb-nav-group${isOpen ? ' tmb-nav-group--open' : ''}`}>
|
||||
<div
|
||||
className={`tmb-nav-group${isOpen ? ' tmb-nav-group--open' : ''}`}
|
||||
onMouseEnter={() => {
|
||||
if (hoverCapableRef.current) openGroup(group.id);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (hoverCapableRef.current) scheduleClose();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`tmb-nav-btn tmb-nav-group-btn${isActive ? ' tmb-nav-btn--active' : ''}${isOpen ? ' tmb-nav-group-btn--open' : ''}`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="menu"
|
||||
onPointerDown={e => {
|
||||
if (hoverCapableRef.current) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenGroupId(prev => (prev === group.id ? null : group.id));
|
||||
|
||||
Reference in New Issue
Block a user