신심리도 추가, 투자심리도 수정

This commit is contained in:
Macbook
2026-05-29 23:03:47 +09:00
parent 320675ea6b
commit 359e6c9653
19 changed files with 226 additions and 74 deletions
@@ -86,7 +86,7 @@ public class IndicatorController {
"ATR", "StandardDeviation", "HistoricalVolatility",
"OBV", "MFI", "ChaikinMF",
"VolumeOscillator", "VR", "Disparity",
"Psychological", "InvestPsychological",
"Psychological", "NewPsychological", "InvestPsychological",
"TRIX", "ADX", "DMI"
)
));
@@ -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<String, List<PlotPoint>> calcNewPsychological(BarSeries s, Map<String, Object> p) {
int period = intP(p, "length", 10);
return Map.of("plot0", newPsychologicalPoints(s, period));
}
private Map<String, List<PlotPoint>> calcInvestPsychological(BarSeries s, Map<String, Object> 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<PlotPoint> psychologicalPoints(BarSeries s, int period, boolean volumeWeighted) {
@@ -805,6 +811,40 @@ public class IndicatorService {
return pts;
}
/** 신심리도: [상승일수×상승폭비율 − 하락일수×하락폭비율] × 100 / N */
private List<PlotPoint> newPsychologicalPoints(BarSeries s, int period) {
int n = s.getBarCount();
ClosePriceIndicator close = new ClosePriceIndicator(s);
List<PlotPoint> 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) {
@@ -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<Num> newPsychIndicator(BarSeries s, int period, boolean volumeWeighted) {
private Indicator<Num> psychIndicator(BarSeries s, int period, boolean volumeWeighted) {
return new PsychologicalIndicator(s, period, volumeWeighted);
}
private Indicator<Num> newSentPsyIndicator(BarSeries s, int period) {
return new NewSentimentPsychologicalIndicator(s, period);
}
/** VR(거래량비율) = 상승일 거래량합 / 하락일 거래량합 × 100 */
private static final class VrIndicator extends CachedIndicator<Num> {
private final int period;
@@ -933,7 +942,7 @@ public class StrategyDslToTa4jAdapter {
}
}
/** 심리도 / 투자심리도 */
/** 심리도 / 투자심리도 */
private static final class PsychologicalIndicator extends CachedIndicator<Num> {
private final int period;
private final boolean volumeWeighted;
@@ -979,6 +988,47 @@ public class StrategyDslToTa4jAdapter {
}
}
/** 신심리도 — 상승·하락 일수와 등락폭 반영 */
private static final class NewSentimentPsychologicalIndicator extends CachedIndicator<Num> {
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]).
+19 -9
View File
@@ -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<Props> = ({ 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:거래량',
+6 -5
View File
@@ -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<string, string> = {
TRIX: 'TRIX_VALUE',
VR: 'VR_VALUE',
PSYCHOLOGICAL: 'PSY_VALUE',
NEW_PSYCHOLOGICAL: 'PSY_VALUE',
NEW_PSYCHOLOGICAL: 'NEW_PSY_VALUE',
INVEST_PSYCHOLOGICAL: 'INVEST_PSY_VALUE',
};
+6 -3
View File
@@ -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<string, string> = {
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':
+37 -11
View File
@@ -113,7 +113,7 @@ export function calcVR(
return { plot0: toPlotData(bars, vals) };
}
/** 심리도(업비트): N일 중 전일 대비 상승일 비율 × 100 */
/** 심리도: N일 중 전일 대비 상승일 비율 × 100 */
export function calcPsychological(
bars: OHLCVBar[],
params: Record<string, number | string | boolean>,
@@ -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<string, number | string | boolean>,
): Record<string, PlotData> {
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<string, number | string | boolean>,
): Record<string, PlotData> {
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) };
}
+1
View File
@@ -166,6 +166,7 @@ const PLOT_LABELS: Record<string, LabelPair> = {
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' },
+2
View File
@@ -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<string, { ko: string; en: string }> =
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' },
};
+1 -1
View File
@@ -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],
};
+9 -8
View File
@@ -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;
@@ -36,6 +36,7 @@ export const INDICATOR_PLOT_PARAMS: Record<string, Record<string, string[]>> = {
UltimateOscillator: { plot0: ['length1', 'length2', 'length3'] },
ConnorsRSI: { plot0: ['rsiLength', 'upDownLength', 'rocLength'] },
Psychological: { plot0: ['length'] },
NewPsychological: { plot0: ['length'] },
InvestPsychological: { plot0: ['length'] },
Disparity: {
plot0: ['ultraLength'],
+2 -2
View File
@@ -7,8 +7,8 @@ import { MAIN_INDICATOR_TYPES, MAIN_INDICATOR_LABELS, isMainIndicatorType } from
const MAIN_SET = new Set<string>(MAIN_INDICATOR_TYPES);
/** 설정 UI에서 제외 (Psychological 과 동일) */
const EXCLUDED_TYPES = new Set(['NewPsychological']);
/** 설정 UI에서 제외할 타입 없음 */
const EXCLUDED_TYPES = new Set<string>();
/** Main 탭 지표 + 그 외 모든 보조지표 type (순서 고정, Main 항목은 하단 registry 에서 제외) */
export function getSettingsIndicatorTypes(): string[] {
+8 -5
View File
@@ -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,6 +21,8 @@ export function normalizePsychologicalParams(
const defaultLen =
type === 'InvestPsychological'
? INVEST_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 };
+23 -10
View File
@@ -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<CondEditorProps> = ({ 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];
@@ -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':
+2 -3
View File
@@ -33,6 +33,7 @@ export const DEFAULT_AUXILIARY_ITEMS: Omit<PaletteItem, 'id' | 'kind'>[] = [
{ 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<string, string> = {
NEW_PSYCHOLOGICAL: 'PSYCHOLOGICAL',
};
const STRATEGY_INDICATOR_TYPE_ALIASES: Record<string, string> = {};
export function getStrategyIndicatorDisplayName(indicatorType: string): string {
const resolved = STRATEGY_INDICATOR_TYPE_ALIASES[indicatorType] ?? indicatorType;
@@ -31,7 +31,7 @@ const DSL_TO_REGISTRY: Record<string, string> = {
VR: 'VR',
DISPARITY: 'Disparity',
PSYCHOLOGICAL: 'Psychological',
NEW_PSYCHOLOGICAL: 'Psychological',
NEW_PSYCHOLOGICAL: 'NewPsychological',
INVEST_PSYCHOLOGICAL: 'InvestPsychological',
BWI: 'BBBandWidth',
DONCHIAN: 'DonchianChannels',
+3 -3
View File
@@ -109,7 +109,7 @@ export const LEFT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
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<string, FieldOption[]> = {
{ 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)' },