diff --git a/backend/src/main/java/com/goldenchart/controller/IndicatorController.java b/backend/src/main/java/com/goldenchart/controller/IndicatorController.java index 19dcdf0..711591e 100644 --- a/backend/src/main/java/com/goldenchart/controller/IndicatorController.java +++ b/backend/src/main/java/com/goldenchart/controller/IndicatorController.java @@ -86,7 +86,7 @@ public class IndicatorController { "ATR", "StandardDeviation", "HistoricalVolatility", "OBV", "MFI", "ChaikinMF", "VolumeOscillator", "VR", "Disparity", - "Psychological", "InvestPsychological", + "Psychological", "NewPsychological", "InvestPsychological", "TRIX", "ADX", "DMI" ) )); diff --git a/backend/src/main/java/com/goldenchart/service/IndicatorService.java b/backend/src/main/java/com/goldenchart/service/IndicatorService.java index da3ee44..5d4401d 100644 --- a/backend/src/main/java/com/goldenchart/service/IndicatorService.java +++ b/backend/src/main/java/com/goldenchart/service/IndicatorService.java @@ -101,7 +101,8 @@ public class IndicatorService { case "VolumeOscillator" -> calcVolumeOscillator(series, p); case "VR" -> calcVR(series, p); case "Disparity" -> calcDisparity(series, p); - case "Psychological", "NewPsychological" -> calcPsychological(series, p); + case "Psychological" -> calcPsychological(series, p); + case "NewPsychological" -> calcNewPsychological(series, p); case "InvestPsychological"-> calcInvestPsychological(series, p); default -> throw new IllegalArgumentException("지원하지 않는 지표: " + type); @@ -762,9 +763,14 @@ public class IndicatorService { return Map.of("plot0", psychologicalPoints(s, period, false)); } + private Map> calcNewPsychological(BarSeries s, Map p) { + int period = intP(p, "length", 10); + return Map.of("plot0", newPsychologicalPoints(s, period)); + } + private Map> calcInvestPsychological(BarSeries s, Map p) { int period = intP(p, "length", 10); - return Map.of("plot0", psychologicalPoints(s, period, true)); + return Map.of("plot0", psychologicalPoints(s, period, false)); } private List psychologicalPoints(BarSeries s, int period, boolean volumeWeighted) { @@ -805,6 +811,40 @@ public class IndicatorService { return pts; } + /** 신심리도: [상승일수×상승폭비율 − 하락일수×하락폭비율] × 100 / N */ + private List newPsychologicalPoints(BarSeries s, int period) { + int n = s.getBarCount(); + ClosePriceIndicator close = new ClosePriceIndicator(s); + List pts = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Bar bar = s.getBar(i); + long t = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds(); + if (i < period) { + pts.add(PlotPoint.builder().time(t).value(null).build()); + continue; + } + double upSize = 0, downSize = 0; + int upDay = 0, downDay = 0; + for (int j = i - period + 1; j <= i; j++) { + Double c = safe(close.getValue(j)); + Double cPrev = safe(close.getValue(j - 1)); + if (c == null || cPrev == null) continue; + double diff = c - cPrev; + if (diff > 0) { + upSize += diff; + upDay++; + } else if (diff < 0) { + downSize += -diff; + downDay++; + } + } + double total = upSize + downSize; + Double val = total == 0 ? null : (upDay * (upSize / total) - downDay * (downSize / total)) * 100.0 / period; + pts.add(PlotPoint.builder().time(t).value(val).build()); + } + return pts; + } + // ── EMA helper (double[], for TSI manual calc) ──────────────────────────── private double[] ema(double[] src, int len, int n) { diff --git a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java index d5ce94b..17b9077 100644 --- a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java +++ b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java @@ -80,7 +80,7 @@ public class StrategyDslToTa4jAdapter { Map.entry("ICHIMOKU", "IchimokuCloud"), Map.entry("VOLUME_OSC", "VolumeOscillator"), Map.entry("PSYCHOLOGICAL", "Psychological"), - Map.entry("NEW_PSYCHOLOGICAL", "Psychological"), + Map.entry("NEW_PSYCHOLOGICAL", "NewPsychological"), Map.entry("INVEST_PSYCHOLOGICAL", "InvestPsychological"), Map.entry("BWI", "BBBandWidth"), Map.entry("VR", "VR"), @@ -700,17 +700,22 @@ public class StrategyDslToTa4jAdapter { } if (field.startsWith("PSY_VALUE_")) { int period = parseTrailingInt(field, "PSY_VALUE_", intP(p, "length", 12)); - return newPsychIndicator(s, period, false); + return psychIndicator(s, period, false); } - if (field.equals("PSY_VALUE") || field.equals("NEW_PSY_VALUE") - || indType.equals("PSYCHOLOGICAL") || indType.equals("NEW_PSYCHOLOGICAL")) - return newPsychIndicator(s, intP(p, "length", 12), false); + if (field.equals("PSY_VALUE") || indType.equals("PSYCHOLOGICAL")) + return psychIndicator(s, intP(p, "length", 12), false); + if (field.startsWith("NEW_PSY_VALUE_")) { + int period = parseTrailingInt(field, "NEW_PSY_VALUE_", intP(p, "length", 10)); + return newSentPsyIndicator(s, period); + } + if (field.equals("NEW_PSY_VALUE") || indType.equals("NEW_PSYCHOLOGICAL")) + return newSentPsyIndicator(s, intP(p, "length", 10)); if (field.startsWith("INVEST_PSY_VALUE_")) { int period = parseTrailingInt(field, "INVEST_PSY_VALUE_", intP(p, "length", 10)); - return newPsychIndicator(s, period, true); + return psychIndicator(s, period, false); } if (field.equals("INVEST_PSY_VALUE") || indType.equals("INVEST_PSYCHOLOGICAL")) - return newPsychIndicator(s, intP(p, "length", 10), true); + return psychIndicator(s, intP(p, "length", 10), false); // Williams %R if (field.startsWith("WILLIAMS_R_VALUE_")) { int period = parseTrailingInt(field, "WILLIAMS_R_VALUE_", intP(p, "length", 14)); @@ -894,10 +899,14 @@ public class StrategyDslToTa4jAdapter { return new VrIndicator(s, period); } - private Indicator newPsychIndicator(BarSeries s, int period, boolean volumeWeighted) { + private Indicator psychIndicator(BarSeries s, int period, boolean volumeWeighted) { return new PsychologicalIndicator(s, period, volumeWeighted); } + private Indicator newSentPsyIndicator(BarSeries s, int period) { + return new NewSentimentPsychologicalIndicator(s, period); + } + /** VR(거래량비율) = 상승일 거래량합 / 하락일 거래량합 × 100 */ private static final class VrIndicator extends CachedIndicator { private final int period; @@ -933,7 +942,7 @@ public class StrategyDslToTa4jAdapter { } } - /** 신심리도 / 투자심리도 */ + /** 심리도 / 투자심리도 */ private static final class PsychologicalIndicator extends CachedIndicator { private final int period; private final boolean volumeWeighted; @@ -979,6 +988,47 @@ public class StrategyDslToTa4jAdapter { } } + /** 신심리도 — 상승·하락 일수와 등락폭 반영 */ + private static final class NewSentimentPsychologicalIndicator extends CachedIndicator { + private final int period; + private final ClosePriceIndicator close; + + NewSentimentPsychologicalIndicator(BarSeries series, int period) { + super(series); + this.period = period; + this.close = new ClosePriceIndicator(series); + } + + @Override + protected Num calculate(int index) { + if (index < period) return null; + double upSize = 0, downSize = 0; + int upDay = 0, downDay = 0; + for (int j = index - period + 1; j <= index; j++) { + Num c = close.getValue(j); + Num cPrev = close.getValue(j - 1); + if (c == null || cPrev == null) continue; + double diff = c.doubleValue() - cPrev.doubleValue(); + if (diff > 0) { + upSize += diff; + upDay++; + } else if (diff < 0) { + downSize += -diff; + downDay++; + } + } + double total = upSize + downSize; + if (total == 0) return null; + double val = (upDay * (upSize / total) - downDay * (downSize / total)) * 100.0 / period; + return getBarSeries().numFactory().numOf(val); + } + + @Override + public int getCountOfUnstableBars() { + return period + 1; + } + } + /** * 직전 N봉 고가 중 최고값 (현재 봉 제외). * index t 에서 max(high[t-N]..high[t-1]). diff --git a/frontend/src/components/StrategyPage.tsx b/frontend/src/components/StrategyPage.tsx index 61ade15..3a8411d 100644 --- a/frontend/src/components/StrategyPage.tsx +++ b/frontend/src/components/StrategyPage.tsx @@ -85,7 +85,7 @@ const DEF_DEFAULTS = { obvPeriod: 9, obvSignal: 9, bbPeriod: 20, bbStdDev: 2, ichTenkan: 9, ichKijun: 26, ichSenkouB: 52, - newPsy: 12, investPsy: 10, + newPsy: 10, psyPeriod: 12, investPsy: 10, williamsR: 14, bwiPeriod: 20, vrPeriod: 10, @@ -101,7 +101,7 @@ const DEF_DEFAULTS = { BWI: { over: 80, mid: 50, under: 20 }, VR: { over: 200, mid: 100, under: 50 }, PSYCHOLOGICAL: { over: 75, mid: 50, under: 25 }, - NEW_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 }, @@ -147,7 +147,8 @@ function buildDef(activeIndicators: IndicatorConfig[]): DefType { const vo = p('VolumeOscillator'); const vrInd = p('VR'); const disp = p('Disparity'); - const nPsy = p('Psychological') ?? p('NewPsychological'); + 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 @@ -175,7 +176,7 @@ function buildDef(activeIndicators: IndicatorConfig[]): DefType { VR: 'VR', DISPARITY: 'Disparity', PSYCHOLOGICAL: 'Psychological', - NEW_PSYCHOLOGICAL: 'Psychological', + NEW_PSYCHOLOGICAL: 'NewPsychological', INVEST_PSYCHOLOGICAL: 'InvestPsychological', OBV: 'OBV', BOLLINGER: 'BollingerBands', @@ -230,6 +231,7 @@ function buildDef(activeIndicators: IndicatorConfig[]): DefType { 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), @@ -385,11 +387,19 @@ const getFieldOpts = (ind: string, signalType: 'buy'|'sell', DEF: DefType): Opt[ { value:`DISPARITY${DEF.dispLong}`, label:`장기 이격도(${DEF.dispLong}일)` }, { value:kv(100), label:'중앙선(100)' }, ]); - case 'PSYCHOLOGICAL': - case 'NEW_PSYCHOLOGICAL': { + case 'PSYCHOLOGICAL': { const { over, mid, under } = th({ over:75, mid:50, under:25 }); return condOpts([ - { value:'PSY_VALUE', label:`심리도 값(${DEF.newPsy}일)` }, + { value:'PSY_VALUE', label:`심리도 값(${DEF.psyPeriod}일)` }, + { value:kv(over), label:`${overTerm}(${over})` }, + { value:kv(mid), label:`중앙선(${mid})` }, + { value:kv(under), label:`${underTerm}(${under})` }, + ]); + } + case 'NEW_PSYCHOLOGICAL': { + const { over, mid, under } = th({ over:50, mid:0, under:-50 }); + return condOpts([ + { value:'NEW_PSY_VALUE', label:`신심리도 값(${DEF.newPsy}일)` }, { value:kv(over), label:`${overTerm}(${over})` }, { value:kv(mid), label:`중앙선(${mid})` }, { value:kv(under), label:`${underTerm}(${under})` }, @@ -502,7 +512,7 @@ const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): EMA: { l:`EMA${DEF.maLines[0]}`, r:`EMA${DEF.maLines[1]}` }, DISPARITY: { l:`DISPARITY${DEF.dispUltra}`, r: mid(100) }, PSYCHOLOGICAL: { l:'PSY_VALUE', r: over(75) }, - NEW_PSYCHOLOGICAL: { l:'PSY_VALUE', r: over(75) }, + NEW_PSYCHOLOGICAL: { l:'NEW_PSY_VALUE', r: over(50) }, INVEST_PSYCHOLOGICAL: { l:'INVEST_PSY_VALUE', r: over(75) }, WILLIAMS_R: { l:'WILLIAMS_R_VALUE', r: over(-20) }, BWI: { l:'BWI_VALUE', r: over(80) }, @@ -1233,7 +1243,7 @@ export const StrategyPage: React.FC = ({ activeIndicators = [] }) => { { type:'indicator' as const, value:'ICHIMOKU', label:'일목균형표', color:'primary' }, ]; const indicatorItems = [ - 'PSYCHOLOGICAL:심리도', 'NEW_PSYCHOLOGICAL:심리도', 'INVEST_PSYCHOLOGICAL:투자심리도', + 'PSYCHOLOGICAL:심리도', 'NEW_PSYCHOLOGICAL:신심리도', 'INVEST_PSYCHOLOGICAL:투자심리도', 'MACD:MACD', 'CCI:CCI', 'ADX:ADX', 'BWI:BWI', 'DMI:DMI', 'OBV:OBV', 'RSI:RSI', 'STOCHASTIC:Stochastic', 'WILLIAMS_R:Williams %R', 'TRIX:TRIX', 'VOLUME_OSC:VolumeOSC', 'VR:VR', 'DISPARITY:이격도', 'VOLUME:거래량', diff --git a/frontend/src/utils/compositeIndicators.ts b/frontend/src/utils/compositeIndicators.ts index 4d5ddf1..56ab5a3 100644 --- a/frontend/src/utils/compositeIndicators.ts +++ b/frontend/src/utils/compositeIndicators.ts @@ -10,6 +10,7 @@ export interface CompositePeriodDef { williamsR: number; trixPeriod: number; vrPeriod: number; + psyPeriod: number; newPsy: number; investPsy: number; dispShort: number; @@ -77,8 +78,8 @@ export function getCompositeDefaultPeriods(ind: string, def: CompositePeriodDef) case 'WILLIAMS_R': return { short: def.williamsR, long: Math.max(def.williamsR * 2, 28) }; case 'TRIX': return { short: def.trixPeriod, long: Math.max(def.trixPeriod * 2, 24) }; case 'VR': return { short: def.vrPeriod, long: Math.max(def.vrPeriod * 2, 20) }; - case 'PSYCHOLOGICAL': - case 'NEW_PSYCHOLOGICAL': return { short: def.newPsy, long: Math.max(def.newPsy * 2, 24) }; + case 'PSYCHOLOGICAL': return { short: def.psyPeriod, long: Math.max(def.psyPeriod * 2, 24) }; + case 'NEW_PSYCHOLOGICAL': return { short: def.newPsy, long: Math.max(def.newPsy * 2, 20) }; case 'INVEST_PSYCHOLOGICAL': return { short: def.investPsy, long: Math.max(def.investPsy * 2, 20) }; case 'DISPARITY': return { short: def.dispShort, long: def.dispMid }; default: return { short: 9, long: 20 }; @@ -95,8 +96,8 @@ export function compositeValueField(ind: string, period: number): string { case 'WILLIAMS_R': return `WILLIAMS_R_VALUE_${period}`; case 'TRIX': return `TRIX_VALUE_${period}`; case 'VR': return `VR_VALUE_${period}`; - case 'PSYCHOLOGICAL': - case 'NEW_PSYCHOLOGICAL': return `PSY_VALUE_${period}`; + case 'PSYCHOLOGICAL': return `PSY_VALUE_${period}`; + case 'NEW_PSYCHOLOGICAL': return `NEW_PSY_VALUE_${period}`; case 'INVEST_PSYCHOLOGICAL': return `INVEST_PSY_VALUE_${period}`; case 'DISPARITY': return `DISPARITY${period}`; default: return `${ind}_VALUE_${period}`; @@ -117,7 +118,7 @@ const COMPOSITE_VALUE_PREFIX: Record = { TRIX: 'TRIX_VALUE', VR: 'VR_VALUE', PSYCHOLOGICAL: 'PSY_VALUE', - NEW_PSYCHOLOGICAL: 'PSY_VALUE', + NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE', INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE', }; diff --git a/frontend/src/utils/conditionPeriods.ts b/frontend/src/utils/conditionPeriods.ts index 73a7b8a..349abaf 100644 --- a/frontend/src/utils/conditionPeriods.ts +++ b/frontend/src/utils/conditionPeriods.ts @@ -42,6 +42,7 @@ export interface IndicatorPeriodDef { trixPeriod: number; vrPeriod: number; newPsy: number; + psyPeriod: number; investPsy: number; } @@ -53,7 +54,7 @@ export const VALUE_FIELD_PREFIX: Record = { TRIX: 'TRIX_VALUE', VR: 'VR_VALUE', PSYCHOLOGICAL: 'PSY_VALUE', - NEW_PSYCHOLOGICAL: 'PSY_VALUE', + NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE', INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE', }; @@ -439,9 +440,10 @@ export function getThresholdPresetOptions(indicatorType: string): number[] { switch (indicatorType) { case 'RSI': case 'PSYCHOLOGICAL': - case 'NEW_PSYCHOLOGICAL': case 'INVEST_PSYCHOLOGICAL': return [20, 25, 30, 50, 70, 75, 80]; + case 'NEW_PSYCHOLOGICAL': + return [-50, -25, 0, 25, 50]; case 'STOCHASTIC': return [20, 50, 80]; case 'CCI': @@ -462,10 +464,11 @@ export function getThresholdBounds(indicatorType: string): { min: number; max: n switch (indicatorType) { case 'RSI': case 'PSYCHOLOGICAL': - case 'NEW_PSYCHOLOGICAL': case 'INVEST_PSYCHOLOGICAL': case 'STOCHASTIC': return { min: 0, max: 100 }; + case 'NEW_PSYCHOLOGICAL': + return { min: -100, max: 100 }; case 'WILLIAMS_R': return { min: -100, max: 0 }; case 'CCI': diff --git a/frontend/src/utils/customIndicators.ts b/frontend/src/utils/customIndicators.ts index 37781b1..2e5dea0 100644 --- a/frontend/src/utils/customIndicators.ts +++ b/frontend/src/utils/customIndicators.ts @@ -113,7 +113,7 @@ export function calcVR( return { plot0: toPlotData(bars, vals) }; } -/** 심리도(업비트): N일 중 전일 대비 상승일 비율 × 100 */ +/** 심리도: N일 중 전일 대비 상승일 비율 × 100 */ export function calcPsychological( bars: OHLCVBar[], params: Record, @@ -131,27 +131,53 @@ export function calcPsychological( return { plot0: toPlotData(bars, vals) }; } -/** @deprecated Psychological 사용 */ -export const calcNewPsychological = calcPsychological; +/** + * 신심리도(업비트): [상승일수×상승폭비율 − 하락일수×하락폭비율] × 100 / N + * 상승폭비율 = 상승폭합 / (상승폭합+하락폭합) + */ +export function calcNewPsychological( + bars: OHLCVBar[], + params: Record, +): Record { + const period = Number(params.length ?? 10); + const closes = bars.map(b => b.close); + const vals = closes.map((_, i) => { + if (i < period) return NaN; + let upSize = 0; + let downSize = 0; + let upDay = 0; + let downDay = 0; + for (let j = i - period + 1; j <= i; j++) { + const diff = closes[j] - closes[j - 1]; + if (diff > 0) { + upSize += diff; + upDay++; + } else if (diff < 0) { + downSize += -diff; + downDay++; + } + } + const total = upSize + downSize; + if (total === 0) return NaN; + return (upDay * (upSize / total) - downDay * (downSize / total)) * 100 / period; + }); + return { plot0: toPlotData(bars, vals) }; +} -/** 투자심리도(업비트): N일 중 상승일 거래량 비중 × 100 */ +/** 투자심리도(업비트): N일 중 전일 대비 상승일 비율 × 100 */ export function calcInvestPsychological( bars: OHLCVBar[], params: Record, ): Record { const period = Number(params.length ?? 10); const closes = bars.map(b => b.close); - const volumes = bars.map(b => b.volume); const vals = closes.map((_, i) => { if (i < period) return NaN; - let upVol = 0; - let total = 0; + let up = 0; for (let j = i - period + 1; j <= i; j++) { - total += volumes[j]; - if (closes[j] > closes[j - 1]) upVol += volumes[j]; + if (closes[j] > closes[j - 1]) up++; } - if (total === 0) return NaN; - return (upVol / total) * 100; + return (up / period) * 100; }); return { plot0: toPlotData(bars, vals) }; } diff --git a/frontend/src/utils/indicatorLabels.ts b/frontend/src/utils/indicatorLabels.ts index 2ac2e04..127603a 100644 --- a/frontend/src/utils/indicatorLabels.ts +++ b/frontend/src/utils/indicatorLabels.ts @@ -166,6 +166,7 @@ const PLOT_LABELS: Record = { HV: { ko: '역사적변동성', en: 'HV' }, OBV: { ko: '잔량균형', en: 'OBV' }, 심리도: { ko: '심리도', en: 'Psychological' }, + 신심리도: { ko: '신심리도', en: 'New Psychological' }, 투자심리도: { ko: '투자심리도', en: 'Invest Psychological' }, MFI: { ko: '자금흐름지수', en: 'MFI' }, CMF: { ko: '차이킨자금흐름', en: 'CMF' }, diff --git a/frontend/src/utils/indicatorMainTab.ts b/frontend/src/utils/indicatorMainTab.ts index fdd9d95..6a3c985 100644 --- a/frontend/src/utils/indicatorMainTab.ts +++ b/frontend/src/utils/indicatorMainTab.ts @@ -30,6 +30,7 @@ export const MAIN_INDICATOR_TYPES: readonly string[] = [ 'VR', 'Disparity', 'Psychological', + 'NewPsychological', 'InvestPsychological', 'SMA', ] as const; @@ -51,6 +52,7 @@ export const MAIN_INDICATOR_LABELS: Record = VR: { ko: 'VR (거래량비율)', en: 'Volume Ratio' }, Disparity: { ko: '이격도', en: 'Disparity' }, Psychological: { ko: '심리도', en: 'Psychological Line' }, + NewPsychological: { ko: '신심리도', en: 'New Psychological Line' }, InvestPsychological: { ko: '투자심리도', en: 'Invest Psychological Line' }, SMA: { ko: '단순 이동평균 (MA1~MA11)', en: 'Simple Moving Average' }, }; diff --git a/frontend/src/utils/indicatorParamSpec.ts b/frontend/src/utils/indicatorParamSpec.ts index ec641ab..1e19abf 100644 --- a/frontend/src/utils/indicatorParamSpec.ts +++ b/frontend/src/utils/indicatorParamSpec.ts @@ -180,7 +180,7 @@ export function getHlinePriceSpec(indicatorType: string, current: number): Numer Disparity: [90, 95, 100, 105, 110], VR: [50, 100, 150, 200, 250], Psychological: [25, 50, 75], - NewPsychological: [25, 50, 75], + NewPsychological: [-50, 0, 50], InvestPsychological: [25, 50, 75], }; diff --git a/frontend/src/utils/indicatorRegistry.ts b/frontend/src/utils/indicatorRegistry.ts index d7fe7c2..ac64cf2 100644 --- a/frontend/src/utils/indicatorRegistry.ts +++ b/frontend/src/utils/indicatorRegistry.ts @@ -352,7 +352,8 @@ export const INDICATOR_REGISTRY: IndicatorDef[] = [ { type:'VR', name:'Volume Ratio', koreanName:'거래량비율', shortName:'VR', category:'Volume', overlay:false, defaultParams:{length:10}, plots:[{id:'plot0',title:'VR', color:'#AB47BC',type:'line',lineWidth:2}], hlines:[{price:200,color:'#EF5350'},{price:100,color:'#607D8B'},{price:50,color:'#4CAF50'}], description:'거래량 비율 (VR)' }, { type:'Disparity', name:'Disparity Index', koreanName:'이격도', shortName:'Disp', category:'Oscillators', overlay:false, defaultParams:{ultraLength:5, shortLength:10, midLength:20, longLength:60, src:'close'}, plots:[{id:'plot0',title:'초단기',color:'#E91E63',type:'line',lineWidth:1},{id:'plot1',title:'단기',color:'#FF9800',type:'line',lineWidth:1},{id:'plot2',title:'중기',color:'#4CAF50',type:'line',lineWidth:1},{id:'plot3',title:'장기',color:'#2196F3',type:'line',lineWidth:1}], hlines:[{price:100,color:'#607D8B'}], description:'이격도 (종가÷이평×100)' }, { type:'Psychological', name:'Psychological Line', koreanName:'심리도', shortName:'Psy', category:'Oscillators', overlay:false, defaultParams:{length:12}, plots:[{id:'plot0',title:'심리도',color:'#00BCD4',type:'line',lineWidth:2}], hlines:[{price:75,color:'#EF5350',label:'과열선'},{price:50,color:'#607D8B',label:'중앙선'},{price:25,color:'#4CAF50',label:'침체선'}], description:'심리도 (상승일 비율)' }, - { type:'InvestPsychological', name:'Invest Psychological Line', koreanName:'투자심리도', shortName:'IPsych',category:'Oscillators', overlay:false, defaultParams:{length:10}, plots:[{id:'plot0',title:'투자심리도',color:'#7E57C2',type:'line',lineWidth:2}], hlines:[{price:75,color:'#EF5350',label:'과열선'},{price:50,color:'#607D8B',label:'중앙선'},{price:25,color:'#4CAF50',label:'침체선'}], description:'투자심리도 (상승일 거래량 비중)' }, + { type:'NewPsychological', name:'New Psychological Line', koreanName:'신심리도', shortName:'NPsy', category:'Oscillators', overlay:false, defaultParams:{length:10}, plots:[{id:'plot0',title:'신심리도',color:'#F44336',type:'line',lineWidth:2}], hlines:[{price:50,color:'#EF5350',label:'과열선'},{price:0,color:'#607D8B',label:'중앙선'},{price:-50,color:'#4CAF50',label:'침체선'}], description:'신심리도 (상승·하락 폭 반영)' }, + { type:'InvestPsychological', name:'Invest Psychological Line', koreanName:'투자심리도', shortName:'IPsych',category:'Oscillators', overlay:false, defaultParams:{length:10}, plots:[{id:'plot0',title:'투자심리도',color:'#7E57C2',type:'line',lineWidth:2}], hlines:[{price:75,color:'#EF5350',label:'과열선'},{price:50,color:'#607D8B',label:'중앙선'},{price:25,color:'#4CAF50',label:'침체선'}], description:'투자심리도 (상승일 비율)' }, { type:'MFI', name:'Money Flow Index', koreanName:'자금흐름지수', shortName:'MFI', category:'Volume', overlay:false, defaultParams:{length:14}, plots:[{id:'plot0',title:'MFI', color:'#00BCD4',type:'line',lineWidth:2}], hlines:[{price:80,color:'#EF5350'},{price:20,color:'#4CAF50'}], description:'자금 흐름 지수' }, { type:'ChaikinMF', name:'Chaikin Money Flow', koreanName:'차이킨자금흐름', shortName:'CMF', category:'Volume', overlay:false, defaultParams:{length:20}, plots:[{id:'plot0',title:'CMF', color:'#4CAF50',type:'histogram'}], hlines:[{price:0.25,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-0.25,color:'#4CAF50'}], description:'차이킨 자금 흐름' }, { type:'ChaikinOscillator', name:'Chaikin Oscillator', koreanName:'차이킨오실레이터', shortName:'CO', category:'Volume', overlay:false, defaultParams:{fastLength:3, slowLength:10}, plots:[{id:'plot0',title:'CO', color:'#FF9800',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'차이킨 오실레이터' }, @@ -414,13 +415,13 @@ export function formatIndicatorDisplayLabel(indicatorType: string): string { /** 실시간 차트 pane·범례 타이틀 (심리도·투자심리도·이격도 등 한글명 우선) */ export function getIndicatorChartTitle(type: string): string { - const resolved = type === 'NewPsychological' ? 'Psychological' : type; - const def = getIndicatorDef(resolved); + const def = getIndicatorDef(type); if (!def) return type; if ( - resolved === 'Psychological' - || resolved === 'InvestPsychological' - || resolved === 'Disparity' + type === 'Psychological' + || type === 'NewPsychological' + || type === 'InvestPsychological' + || type === 'Disparity' ) { return def.koreanName; } @@ -491,7 +492,7 @@ export function enrichIndicatorConfig( if (cfgNorm.type === 'OBV') { params = normalizeObvParams(params); } - if (cfgNorm.type === 'Psychological' || cfgNorm.type === 'InvestPsychological') { + if (cfgNorm.type === 'Psychological' || cfgNorm.type === 'NewPsychological' || cfgNorm.type === 'InvestPsychological') { params = normalizePsychologicalParams(cfgNorm.type, params); } if (cfgNorm.type === 'BollingerBands') { @@ -587,7 +588,7 @@ async function loadCustomCalculators() { CUSTOM_CALCULATORS.VR = c.calcVR; CUSTOM_CALCULATORS.Disparity = c.calcDisparityUpbit; CUSTOM_CALCULATORS.Psychological = c.calcPsychological; - CUSTOM_CALCULATORS.NewPsychological = c.calcPsychological; + CUSTOM_CALCULATORS.NewPsychological = c.calcNewPsychological; CUSTOM_CALCULATORS.InvestPsychological = c.calcInvestPsychological; CUSTOM_CALCULATORS.BollingerBands = c.calcBollingerBands; CUSTOM_CALCULATORS.DMI = c.calcDMI; diff --git a/frontend/src/utils/indicatorSettingsLayout.ts b/frontend/src/utils/indicatorSettingsLayout.ts index 9083f0f..79f56e7 100644 --- a/frontend/src/utils/indicatorSettingsLayout.ts +++ b/frontend/src/utils/indicatorSettingsLayout.ts @@ -36,6 +36,7 @@ export const INDICATOR_PLOT_PARAMS: Record> = { UltimateOscillator: { plot0: ['length1', 'length2', 'length3'] }, ConnorsRSI: { plot0: ['rsiLength', 'upDownLength', 'rocLength'] }, Psychological: { plot0: ['length'] }, + NewPsychological: { plot0: ['length'] }, InvestPsychological: { plot0: ['length'] }, Disparity: { plot0: ['ultraLength'], diff --git a/frontend/src/utils/indicatorSettingsList.ts b/frontend/src/utils/indicatorSettingsList.ts index 8486326..1f9bd5b 100644 --- a/frontend/src/utils/indicatorSettingsList.ts +++ b/frontend/src/utils/indicatorSettingsList.ts @@ -7,8 +7,8 @@ import { MAIN_INDICATOR_TYPES, MAIN_INDICATOR_LABELS, isMainIndicatorType } from const MAIN_SET = new Set(MAIN_INDICATOR_TYPES); -/** 설정 UI에서 제외 (Psychological 과 동일) */ -const EXCLUDED_TYPES = new Set(['NewPsychological']); +/** 설정 UI에서 제외할 타입 없음 */ +const EXCLUDED_TYPES = new Set(); /** Main 탭 지표 + 그 외 모든 보조지표 type (순서 고정, Main 항목은 하단 registry 에서 제외) */ export function getSettingsIndicatorTypes(): string[] { diff --git a/frontend/src/utils/psychologicalConfig.ts b/frontend/src/utils/psychologicalConfig.ts index f27d3b3..87906dc 100644 --- a/frontend/src/utils/psychologicalConfig.ts +++ b/frontend/src/utils/psychologicalConfig.ts @@ -1,15 +1,16 @@ /** - * 심리도 / 투자심리도 (업비트) - * - 심리도: N일 중 종가 상승일 비율 × 100 - * - 투자심리도: N일 중 상승일 거래량 비중 × 100 + * 심리도 / 신심리도 / 투자심리도 (업비트) + * - 심리도: N일 중 종가 상승일 비율 × 100 (0~100) + * - 신심리도: N일 상승·하락 일수와 등락폭 반영, 0 중심 (미래에셋 공식) + * - 투자심리도: N일 중 종가 상승일 비율 × 100 (업비트·한국경제, 기본 10일) */ export const PSYCHOLOGICAL_DEFAULT_LENGTH = 12; +export const NEW_PSYCHOLOGICAL_DEFAULT_LENGTH = 10; export const INVEST_PSYCHOLOGICAL_DEFAULT_LENGTH = 10; -/** 레거시 type → 업비트 명칭 type */ +/** 레거시 type 정규화 — NewPsychological 은 별도 지표로 유지 */ export function resolvePsychologicalIndicatorType(type: string): string { - if (type === 'NewPsychological') return 'Psychological'; return type; } @@ -20,7 +21,9 @@ export function normalizePsychologicalParams( const defaultLen = type === 'InvestPsychological' ? INVEST_PSYCHOLOGICAL_DEFAULT_LENGTH - : PSYCHOLOGICAL_DEFAULT_LENGTH; + : type === 'NewPsychological' + ? NEW_PSYCHOLOGICAL_DEFAULT_LENGTH + : PSYCHOLOGICAL_DEFAULT_LENGTH; const length = Math.max(1, Number(params.length ?? defaultLen) || defaultLen); return { ...params, length }; } diff --git a/frontend/src/utils/strategyEditorShared.tsx b/frontend/src/utils/strategyEditorShared.tsx index 8c8eba9..85b567d 100644 --- a/frontend/src/utils/strategyEditorShared.tsx +++ b/frontend/src/utils/strategyEditorShared.tsx @@ -102,7 +102,7 @@ const DEF_DEFAULTS = { obvPeriod: 9, obvSignal: 9, bbPeriod: 20, bbStdDev: 2, ichTenkan: 9, ichKijun: 26, ichSenkouB: 52, - newPsy: 12, investPsy: 10, + newPsy: 10, psyPeriod: 12, investPsy: 10, williamsR: 14, bwiPeriod: 20, vrPeriod: 10, @@ -118,7 +118,7 @@ const DEF_DEFAULTS = { BWI: { over: 80, mid: 50, under: 20 }, VR: { over: 200, mid: 100, under: 50 }, PSYCHOLOGICAL: { over: 75, mid: 50, under: 25 }, - NEW_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 }, @@ -165,7 +165,8 @@ export function buildDef(activeIndicators: IndicatorConfig[]): DefType { const vo = p('VolumeOscillator'); const vrInd = p('VR'); const disp = p('Disparity'); - const nPsy = p('Psychological') ?? p('NewPsychological'); + 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 @@ -193,7 +194,7 @@ export function buildDef(activeIndicators: IndicatorConfig[]): DefType { VR: 'VR', DISPARITY: 'Disparity', PSYCHOLOGICAL: 'Psychological', - NEW_PSYCHOLOGICAL: 'Psychological', + NEW_PSYCHOLOGICAL: 'NewPsychological', INVEST_PSYCHOLOGICAL: 'InvestPsychological', OBV: 'OBV', BOLLINGER: 'BollingerBands', @@ -251,6 +252,7 @@ export function buildDef(activeIndicators: IndicatorConfig[]): DefType { 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), @@ -448,12 +450,11 @@ export const getFieldOpts = ( { value:THRESHOLD_HL_MID, label:`중앙선(${mid})` }, ]); } - case 'PSYCHOLOGICAL': - case 'NEW_PSYCHOLOGICAL': { + case 'PSYCHOLOGICAL': { const { over, mid, under } = th({ over:75, mid:50, under:25 }); const psyP = cond ? getConditionValuePeriod({ ...cond, indicatorType: ind }, DEF) - : DEF.newPsy; + : DEF.psyPeriod; return condOpts([ { value:'PSY_VALUE', label:`심리도 라인(${psyP}일)` }, { value:THRESHOLD_HL_OVER, label:`${overTerm}(${over})` }, @@ -461,6 +462,18 @@ export const getFieldOpts = ( { value:THRESHOLD_HL_UNDER, label:`${underTerm}(${under})` }, ]); } + 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})` }, + ]); + } case 'INVEST_PSYCHOLOGICAL': { const { over, mid, under } = th({ over:75, mid:50, under:25 }); const ipsyP = cond @@ -591,7 +604,7 @@ const getDefaultConditionFields = (ind: string, signalType: 'buy'|'sell', DEF: D EMA: { l:`EMA${DEF.maLines[0]}`, r:`EMA${DEF.maLines[1]}` }, DISPARITY: { l:`DISPARITY${DEF.dispUltra}`, r: THRESHOLD_HL_MID }, PSYCHOLOGICAL: { l:'PSY_VALUE', r: THRESHOLD_HL_OVER }, - NEW_PSYCHOLOGICAL: { l:'PSY_VALUE', r: THRESHOLD_HL_OVER }, + NEW_PSYCHOLOGICAL: { l:'NEW_PSY_VALUE', r: THRESHOLD_HL_OVER }, INVEST_PSYCHOLOGICAL: { l:'INVEST_PSY_VALUE', r: THRESHOLD_HL_OVER }, WILLIAMS_R: { l:'WILLIAMS_R_VALUE', r: THRESHOLD_HL_OVER }, BWI: { l:'BWI_VALUE', r: THRESHOLD_HL_OVER }, @@ -669,7 +682,7 @@ export function resolveFieldOptionValue(indicatorType: string, field?: string): TRIX: 'TRIX_VALUE', VR: 'VR_VALUE', PSYCHOLOGICAL: 'PSY_VALUE', - NEW_PSYCHOLOGICAL: 'PSY_VALUE', + NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE', INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE', }; const base = bases[indicatorType]; @@ -1020,7 +1033,7 @@ export const CondEditor: React.FC = ({ cond, signalType, onChan TRIX: 'TRIX_VALUE', VR: 'VR_VALUE', PSYCHOLOGICAL: 'PSY_VALUE', - NEW_PSYCHOLOGICAL: 'PSY_VALUE', + NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE', INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE', }; const base = bases[normalized.indicatorType]; diff --git a/frontend/src/utils/strategyIndicatorPeriods.ts b/frontend/src/utils/strategyIndicatorPeriods.ts index d2f3d7d..b826bc8 100644 --- a/frontend/src/utils/strategyIndicatorPeriods.ts +++ b/frontend/src/utils/strategyIndicatorPeriods.ts @@ -36,6 +36,7 @@ export function formatIndicatorPeriodLabel(indicator: string, def: DefType): str case 'DISPARITY': return `${def.dispUltra}/${def.dispShort}/${def.dispMid}/${def.dispLong}`; case 'PSYCHOLOGICAL': + return String(def.psyPeriod); case 'NEW_PSYCHOLOGICAL': return String(def.newPsy); case 'INVEST_PSYCHOLOGICAL': @@ -88,6 +89,7 @@ export function getIndicatorPrimaryPeriod(indicator: string, def: DefType): numb case 'DISPARITY': return def.dispShort; case 'PSYCHOLOGICAL': + return def.psyPeriod; case 'NEW_PSYCHOLOGICAL': return def.newPsy; case 'INVEST_PSYCHOLOGICAL': diff --git a/frontend/src/utils/strategyPaletteStorage.ts b/frontend/src/utils/strategyPaletteStorage.ts index 1b53620..d2bea15 100644 --- a/frontend/src/utils/strategyPaletteStorage.ts +++ b/frontend/src/utils/strategyPaletteStorage.ts @@ -33,6 +33,7 @@ export const DEFAULT_AUXILIARY_ITEMS: Omit[] = [ { value: 'VR', label: 'VR', desc: '거래량 비율', builtIn: true }, { value: 'DISPARITY', label: '이격도', desc: 'Disparity', builtIn: true }, { value: 'PSYCHOLOGICAL', label: '심리도', desc: 'Psychological', builtIn: true }, + { value: 'NEW_PSYCHOLOGICAL', label: '신심리도', desc: 'New Psychological', builtIn: true }, { value: 'INVEST_PSYCHOLOGICAL', label: '투자심리도', desc: 'Invest PSY', builtIn: true }, { value: 'VOLUME', label: '거래량', desc: 'Volume', builtIn: true }, { value: 'NEW_HIGH', label: '신고가', desc: '직전 N봉 최고가 돌파 — 종가/고가 vs N일 신고가선 (9·20일)', builtIn: true, period: 9 }, @@ -145,9 +146,7 @@ export function resetPaletteItems(kind: PaletteItemKind): PaletteItem[] { } /** 전략편집기·전략빌더에 표시할 지표명 (우측 팔레트 label 우선) */ -const STRATEGY_INDICATOR_TYPE_ALIASES: Record = { - NEW_PSYCHOLOGICAL: 'PSYCHOLOGICAL', -}; +const STRATEGY_INDICATOR_TYPE_ALIASES: Record = {}; export function getStrategyIndicatorDisplayName(indicatorType: string): string { const resolved = STRATEGY_INDICATOR_TYPE_ALIASES[indicatorType] ?? indicatorType; diff --git a/frontend/src/utils/strategyToChartIndicators.ts b/frontend/src/utils/strategyToChartIndicators.ts index 8783bda..d8a84a3 100644 --- a/frontend/src/utils/strategyToChartIndicators.ts +++ b/frontend/src/utils/strategyToChartIndicators.ts @@ -31,7 +31,7 @@ const DSL_TO_REGISTRY: Record = { VR: 'VR', DISPARITY: 'Disparity', PSYCHOLOGICAL: 'Psychological', - NEW_PSYCHOLOGICAL: 'Psychological', + NEW_PSYCHOLOGICAL: 'NewPsychological', INVEST_PSYCHOLOGICAL: 'InvestPsychological', BWI: 'BBBandWidth', DONCHIAN: 'DonchianChannels', diff --git a/frontend/src/utils/strategyTypes.ts b/frontend/src/utils/strategyTypes.ts index 21b6056..248bac3 100644 --- a/frontend/src/utils/strategyTypes.ts +++ b/frontend/src/utils/strategyTypes.ts @@ -109,7 +109,7 @@ export const LEFT_FIELD_OPTIONS: Record = { VR: [{ value: 'CURRENT', label: 'VR' }], DISPARITY: [{ value: 'CURRENT', label: '이격도' }], PSYCHOLOGICAL: [{ value: 'CURRENT', label: '심리도' }], - NEW_PSYCHOLOGICAL: [{ value: 'CURRENT', label: '심리도' }], + NEW_PSYCHOLOGICAL: [{ value: 'CURRENT', label: '신심리도' }], INVEST_PSYCHOLOGICAL: [{ value: 'CURRENT', label: '투자심리도' }], VOLUME: [{ value: 'CURRENT', label: '거래량' }], MA: [ @@ -181,8 +181,8 @@ export const RIGHT_FIELD_OPTIONS: Record = { { value: 'OVERSOLD_25', label: '과매도(25)' }, { value: 'CUSTOM', label: '직접 입력' }, ], NEW_PSYCHOLOGICAL: [ - { value: 'OVERBOUGHT_75', label: '과매수(75)' }, { value: 'NEUTRAL_50', label: '중립(50)' }, - { value: 'OVERSOLD_25', label: '과매도(25)' }, { value: 'CUSTOM', label: '직접 입력' }, + { value: 'OVERBOUGHT_50', label: '과매수(50)' }, { value: 'NEUTRAL_0', label: '중립(0)' }, + { value: 'OVERSOLD_NEG50', label: '과매도(-50)' }, { value: 'CUSTOM', label: '직접 입력' }, ], INVEST_PSYCHOLOGICAL: [ { value: 'OVERBOUGHT_75', label: '과매수(75)' }, { value: 'NEUTRAL_50', label: '중립(50)' },