보조지표 소수점 추가, cci 수정
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.Indicator;
|
||||
import org.ta4j.core.indicators.CachedIndicator;
|
||||
import org.ta4j.core.indicators.averages.SMAIndicator;
|
||||
import org.ta4j.core.indicators.statistics.MeanDeviationIndicator;
|
||||
import org.ta4j.core.num.Num;
|
||||
|
||||
/**
|
||||
* TradingView/업비트 CCI — 임의 가격 소스에 대해
|
||||
* (src - SMA) / (0.015 × MeanDeviation) 계산.
|
||||
*/
|
||||
public class CciOnSourceIndicator extends CachedIndicator<Num> {
|
||||
|
||||
private final Indicator<Num> price;
|
||||
private final SMAIndicator sma;
|
||||
private final MeanDeviationIndicator meanDeviation;
|
||||
private final Num factor;
|
||||
private final int barCount;
|
||||
|
||||
public CciOnSourceIndicator(Indicator<Num> price, int barCount) {
|
||||
super(price);
|
||||
this.price = price;
|
||||
this.barCount = barCount;
|
||||
this.factor = getBarSeries().numFactory().numOf(0.015);
|
||||
this.sma = new SMAIndicator(price, barCount);
|
||||
this.meanDeviation = new MeanDeviationIndicator(price, barCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Num calculate(int index) {
|
||||
final Num meanDeviation = this.meanDeviation.getValue(index);
|
||||
if (meanDeviation.isZero()) {
|
||||
return getBarSeries().numFactory().zero();
|
||||
}
|
||||
return price.getValue(index).minus(sma.getValue(index))
|
||||
.dividedBy(meanDeviation.multipliedBy(factor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountOfUnstableBars() {
|
||||
return barCount - 1;
|
||||
}
|
||||
}
|
||||
@@ -396,10 +396,10 @@ public class IndicatorService {
|
||||
*/
|
||||
private Map<String, List<PlotPoint>> calcCCI(BarSeries s, Map<String, Object> p) {
|
||||
int len = intP(p, "length", 13);
|
||||
int maLen = intP(p, "maLength", 20);
|
||||
int maLen = intP(p, "maLength", 10);
|
||||
String maType = p.getOrDefault("maType", "SMA").toString();
|
||||
|
||||
CCIIndicator cci = new CCIIndicator(s, len);
|
||||
CciOnSourceIndicator cci = new CciOnSourceIndicator(src(s, p), len);
|
||||
|
||||
Indicator<Num> maInd = switch (maType) {
|
||||
case "EMA" -> new EMAIndicator(cci, maLen);
|
||||
|
||||
@@ -599,10 +599,11 @@ public class StrategyDslToTa4jAdapter {
|
||||
// CCI — 기간 접미사(CCI_VALUE_13) 또는 기본 CCI_VALUE
|
||||
if (field.startsWith("CCI_VALUE_")) {
|
||||
int period = parseTrailingInt(field, "CCI_VALUE_", intP(p, "length", 13));
|
||||
return new CCIIndicator(s, period);
|
||||
return new CciOnSourceIndicator(resolvePriceSource(s, p), period);
|
||||
}
|
||||
if (field.equals("CCI_VALUE") || indType.equals("CCI"))
|
||||
return new CCIIndicator(s, periodOverride > 0 ? periodOverride : intP(p, "length", 13));
|
||||
return new CciOnSourceIndicator(resolvePriceSource(s, p),
|
||||
periodOverride > 0 ? periodOverride : intP(p, "length", 13));
|
||||
// RSI — 기간 접미사(RSI_VALUE_9) 또는 기본 RSI_VALUE
|
||||
if (field.startsWith("RSI_VALUE_")) {
|
||||
int period = parseTrailingInt(field, "RSI_VALUE_", intP(p, "length", 14));
|
||||
@@ -865,6 +866,17 @@ public class StrategyDslToTa4jAdapter {
|
||||
|
||||
// ── TRIX 헬퍼 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Indicator<Num> resolvePriceSource(BarSeries s, Map<String, Object> p) {
|
||||
return switch (p.getOrDefault("src", "close").toString()) {
|
||||
case "open" -> new OpenPriceIndicator(s);
|
||||
case "high" -> new HighPriceIndicator(s);
|
||||
case "low" -> new LowPriceIndicator(s);
|
||||
case "hl2" -> new MedianPriceIndicator(s);
|
||||
case "hlc3" -> new TypicalPriceIndicator(s);
|
||||
default -> new ClosePriceIndicator(s);
|
||||
};
|
||||
}
|
||||
|
||||
private EMAIndicator tripleEma(ClosePriceIndicator close, int len) {
|
||||
return new EMAIndicator(new EMAIndicator(new EMAIndicator(close, len), len), len);
|
||||
}
|
||||
|
||||
@@ -2357,6 +2357,8 @@ function App() {
|
||||
onDuplicateIndicator={handleDuplicateIndicator}
|
||||
focusedIndicatorId={focusedIndicatorId}
|
||||
onRestoreIndicators={handleRestoreIndicators}
|
||||
onToggleIndicatorHidden={handleToggleIndicatorHidden}
|
||||
onOpenIndicatorSettings={id => setSettingsModalId(id)}
|
||||
volumeVisible={chartVolumeVisible}
|
||||
paneSeparatorOptions={chartPaneSeparator}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,10 @@ export interface ChartRightToolbarProps {
|
||||
onRestore?: () => void;
|
||||
focusedId?: string | null;
|
||||
onDragStart?: (id: string, e: React.PointerEvent) => void;
|
||||
/** 지표명 툴바 — 표시/숨기기 */
|
||||
onToggleHidden?: (id: string) => void;
|
||||
/** 지표명 툴바 — 설정 모달 */
|
||||
onSettings?: (id: string) => void;
|
||||
}
|
||||
|
||||
const BTN_SIZE = 32;
|
||||
@@ -89,6 +93,25 @@ const IconMore = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconEye = ({ hidden }: { hidden?: boolean }) => hidden ? (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M1 7 Q4 2.5 7 2.5 Q10 2.5 13 7 Q10 11.5 7 11.5 Q4 11.5 1 7 Z" strokeOpacity="0.35"/>
|
||||
<line x1="2" y1="2" x2="12" y2="12"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M1 7 Q4 2.5 7 2.5 Q10 2.5 13 7 Q10 11.5 7 11.5 Q4 11.5 1 7 Z"/>
|
||||
<circle cx="7" cy="7" r="2"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconGear = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4">
|
||||
<circle cx="7" cy="7" r="2.3"/>
|
||||
<path d="M7 1 L7.7 2.8 L9.8 2 L9.8 4.2 L11.8 5 L11 7 L11.8 9 L9.8 9.8 L9.8 12 L7.7 11.2 L7 13 L6.3 11.2 L4.2 12 L4.2 9.8 L2.2 9 L3 7 L2.2 5 L4.2 4.2 L4.2 2 L6.3 2.8 Z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface MenuItemDef {
|
||||
key: string;
|
||||
title: string;
|
||||
@@ -109,6 +132,8 @@ function buildMenuItems(
|
||||
onExpand?: (id: string) => void;
|
||||
onRestore?: () => void;
|
||||
onRemove?: (id: string) => void;
|
||||
onToggleHidden?: (id: string) => void;
|
||||
onSettings?: (id: string) => void;
|
||||
onDone?: () => void;
|
||||
},
|
||||
): MenuItemDef[] {
|
||||
@@ -119,9 +144,28 @@ function buildMenuItems(
|
||||
const isFocused = opts.focusedId === item.id;
|
||||
const showExpand = !!(opts.onExpand || (isFocused && opts.onRestore));
|
||||
const done = opts.onDone ?? (() => {});
|
||||
const cfg = indicators.find(ind => ind.id === item.id);
|
||||
const isHidden = cfg?.hidden === true;
|
||||
|
||||
const items: MenuItemDef[] = [];
|
||||
|
||||
if (opts.onToggleHidden) {
|
||||
items.push({
|
||||
key: 'visibility',
|
||||
title: isHidden ? '표시' : '숨기기',
|
||||
icon: <IconEye hidden={isHidden} />,
|
||||
active: isHidden,
|
||||
onClick: () => { opts.onToggleHidden?.(item.id); done(); },
|
||||
});
|
||||
}
|
||||
if (opts.onSettings) {
|
||||
items.push({
|
||||
key: 'settings',
|
||||
title: '지표 설정',
|
||||
icon: <IconGear />,
|
||||
onClick: () => { opts.onSettings?.(item.id); done(); },
|
||||
});
|
||||
}
|
||||
if (showSplit) {
|
||||
items.push({
|
||||
key: 'split',
|
||||
@@ -168,7 +212,7 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
manager, indicators, chartHeight, paused = false,
|
||||
onSplit, onRemove, onDuplicate,
|
||||
onExpand, onRestore, focusedId,
|
||||
onDragStart,
|
||||
onDragStart, onToggleHidden, onSettings,
|
||||
}) => {
|
||||
const [paneLayouts, setPaneLayouts] = useState<Array<{ paneIndex: number; topY: number; height: number }>>([]);
|
||||
const [paneItems, setPaneItems] = useState<ReturnType<typeof buildPaneItems>>([]);
|
||||
@@ -263,6 +307,7 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
const clusterTop = paneBottom - 4 - clusterHeight - stackIdx * (clusterHeight + 4);
|
||||
const menuItems = buildMenuItems(item, indicators, paneItems, {
|
||||
focusedId, onSplit, onDuplicate, onExpand, onRestore, onRemove,
|
||||
onToggleHidden, onSettings,
|
||||
});
|
||||
|
||||
if (menuItems.length === 0 && !allowDrag) return null;
|
||||
@@ -306,6 +351,7 @@ const ChartRightToolbar: React.FC<ChartRightToolbarProps> = ({
|
||||
{openItem && openMenuId && (() => {
|
||||
const menuItems = buildMenuItems(openItem, indicators, paneItems, {
|
||||
focusedId, onSplit, onDuplicate, onExpand, onRestore, onRemove,
|
||||
onToggleHidden, onSettings,
|
||||
onDone: closeFlyout,
|
||||
});
|
||||
if (menuItems.length === 0) return null;
|
||||
|
||||
@@ -949,6 +949,8 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
onDuplicateIndicator={handleDuplicateIndicator}
|
||||
focusedIndicatorId={focusedIndicatorId}
|
||||
onRestoreIndicators={handleRestoreIndicators}
|
||||
onToggleIndicatorHidden={handleToggleHidden}
|
||||
onOpenIndicatorSettings={id => setSettingsModalId(id)}
|
||||
onFullView={onFullView ? () => onFullView(symbolRef.current) : undefined}
|
||||
/>
|
||||
|
||||
|
||||
@@ -120,6 +120,10 @@ interface TradingChartProps {
|
||||
focusedIndicatorId?: string | null;
|
||||
/** 전체화면 → 전체 보기 복원 */
|
||||
onRestoreIndicators?: () => void;
|
||||
/** 지표 표시/숨기기 */
|
||||
onToggleIndicatorHidden?: (id: string) => void;
|
||||
/** 지표 설정 모달 열기 */
|
||||
onOpenIndicatorSettings?: (id: string) => void;
|
||||
/** 우클릭 메뉴에서 매수·매도 선택 시 */
|
||||
onTradeOrderRequest?: (req: { market: string; price: number; side: 'buy' | 'sell' }) => void;
|
||||
/** 표시 시간대 (IANA) */
|
||||
@@ -164,6 +168,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
onDuplicateIndicator,
|
||||
focusedIndicatorId,
|
||||
onRestoreIndicators,
|
||||
onToggleIndicatorHidden,
|
||||
onOpenIndicatorSettings,
|
||||
onTradeOrderRequest,
|
||||
displayTimezone = DEFAULT_DISPLAY_TIMEZONE,
|
||||
volumeVisible = true,
|
||||
@@ -1391,6 +1397,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
onRestore={onRestoreIndicators}
|
||||
focusedId={focusedIndicatorId}
|
||||
onDragStart={(id, e) => paneDragRef.current?.(id, e)}
|
||||
onToggleHidden={onToggleIndicatorHidden}
|
||||
onSettings={onOpenIndicatorSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { IndicatorFillPrimitive } from './IndicatorFillPrimitive';
|
||||
import { BollingerBandFillPrimitive } from './BollingerBandFillPrimitive';
|
||||
import { resolveBbBandBackground } from './bollingerConfig';
|
||||
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, Timeframe } from '../types';
|
||||
import { formatChartAxisPrice } from './dataGenerator';
|
||||
import { formatChartAxisPrice, formatIndicatorAxisPrice } from './dataGenerator';
|
||||
import {
|
||||
DEFAULT_CHART_TIME_FORMAT,
|
||||
formatUnixWithChartPattern,
|
||||
@@ -42,6 +42,16 @@ const MAIN_PRICE_FORMAT = {
|
||||
minMove: 1,
|
||||
formatter: formatChartAxisPrice,
|
||||
};
|
||||
|
||||
const INDICATOR_PRICE_FORMAT = {
|
||||
type: 'custom' as const,
|
||||
minMove: 0.01,
|
||||
formatter: formatIndicatorAxisPrice,
|
||||
};
|
||||
|
||||
function priceFormatForIndicatorPane(paneIndex: number) {
|
||||
return paneIndex >= 2 ? INDICATOR_PRICE_FORMAT : undefined;
|
||||
}
|
||||
import {
|
||||
getIchimokuPlotTitle,
|
||||
isIchimokuCloudVisible,
|
||||
@@ -655,8 +665,12 @@ export class ChartManager {
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
let series: ISeriesApi<SeriesType>;
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
if (plotDef.type === 'histogram') {
|
||||
series = this.chart.addSeries(HistogramSeries, { color: plotDef.color }, pane);
|
||||
series = this.chart.addSeries(HistogramSeries, {
|
||||
color: plotDef.color,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
series.setData(mapHistogramSeriesData(filtered, plotDef));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
|
||||
} else {
|
||||
@@ -671,6 +685,7 @@ export class ChartManager {
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||
@@ -2263,8 +2278,13 @@ export class ChartManager {
|
||||
if (!plot) return;
|
||||
const indicatorVisible = config.hidden !== true;
|
||||
const visible = indicatorVisible && config.plotVisibility?.[plot.id] !== false;
|
||||
const paneIdx = entry.paneIndex ?? 0;
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(paneIdx);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const opts: Record<string, unknown> = { visible };
|
||||
if (indicatorPriceFormat) {
|
||||
opts['priceFormat'] = indicatorPriceFormat;
|
||||
}
|
||||
if (!entry.seriesMeta[i]?.isHistogram) {
|
||||
opts['color'] = plot.color ?? '#aaa';
|
||||
if (plot.lineWidth != null) opts['lineWidth'] = plot.lineWidth;
|
||||
@@ -2274,7 +2294,6 @@ export class ChartManager {
|
||||
plotAllows = entry.seriesMeta[i]?.plotId !== 'plot2';
|
||||
}
|
||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, plotAllows);
|
||||
const paneIdx = entry.paneIndex ?? 0;
|
||||
opts['lastValueVisible'] = showPriceLabel;
|
||||
opts['title'] = paneIdx < 2 && showPriceLabel
|
||||
? this.seriesTitleForPlot(config, plot, plotAllows)
|
||||
|
||||
@@ -15,6 +15,20 @@ function sma(values: number[], period: number): number[] {
|
||||
});
|
||||
}
|
||||
|
||||
function wma(values: number[], period: number): number[] {
|
||||
return values.map((_, i) => {
|
||||
if (i < period - 1) return NaN;
|
||||
let wSum = 0;
|
||||
let w = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
const weight = j + 1;
|
||||
wSum += values[i - period + 1 + j] * weight;
|
||||
w += weight;
|
||||
}
|
||||
return wSum / w;
|
||||
});
|
||||
}
|
||||
|
||||
function ema(values: number[], period: number): number[] {
|
||||
const out = new Array<number>(values.length).fill(NaN);
|
||||
if (values.length < period) return out;
|
||||
@@ -182,6 +196,104 @@ export function calcInvestPsychological(
|
||||
return { plot0: toPlotData(bars, vals) };
|
||||
}
|
||||
|
||||
/**
|
||||
* RSI + 신호선 (업비트·TradingView Wilder RSI)
|
||||
*/
|
||||
export function calcRSI(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const len = Math.max(1, Number(params.length ?? 14) || 14);
|
||||
const maLen = Math.max(1, Number(params.maLength ?? 14) || 14);
|
||||
const rawMa = String(params.maType ?? 'SMA');
|
||||
const maType = rawMa === 'EMA' || rawMa === 'WMA' ? rawMa : (rawMa === 'None' ? 'None' : 'SMA');
|
||||
const srcKey = String(params.src ?? 'close');
|
||||
const prices = bars.map(b => sourceAt(b, srcKey));
|
||||
|
||||
const rsi = new Array<number>(prices.length).fill(NaN);
|
||||
if (prices.length > len) {
|
||||
let gain = 0;
|
||||
let loss = 0;
|
||||
for (let i = 1; i <= len; i++) {
|
||||
const d = prices[i] - prices[i - 1];
|
||||
if (d > 0) gain += d;
|
||||
else loss -= d;
|
||||
}
|
||||
let avgG = gain / len;
|
||||
let avgL = loss / len;
|
||||
rsi[len] = avgL === 0 ? 100 : 100 - 100 / (1 + avgG / avgL);
|
||||
for (let i = len + 1; i < prices.length; i++) {
|
||||
const d = prices[i] - prices[i - 1];
|
||||
const g = d > 0 ? d : 0;
|
||||
const l = d < 0 ? -d : 0;
|
||||
avgG = (avgG * (len - 1) + g) / len;
|
||||
avgL = (avgL * (len - 1) + l) / len;
|
||||
rsi[i] = avgL === 0 ? 100 : 100 - 100 / (1 + avgG / avgL);
|
||||
}
|
||||
}
|
||||
|
||||
let signal: number[];
|
||||
if (maType === 'None') {
|
||||
signal = rsi.map(() => NaN);
|
||||
} else if (maType === 'EMA') {
|
||||
signal = ema(rsi.map(v => (Number.isFinite(v) ? v : NaN)), maLen);
|
||||
} else if (maType === 'WMA') {
|
||||
signal = wma(rsi.map(v => (Number.isFinite(v) ? v : NaN)), maLen);
|
||||
} else {
|
||||
signal = sma(rsi, maLen);
|
||||
}
|
||||
|
||||
return {
|
||||
plot0: toPlotData(bars, rsi),
|
||||
plot1: toPlotData(bars, signal),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* CCI + 신호선 (업비트·TradingView)
|
||||
* CCI = (src - SMA(src,N)) / (0.015 × MeanDeviation)
|
||||
* 신호선 = CCI의 SMA/EMA/WMA (기본 SMA 10)
|
||||
*/
|
||||
export function calcCCI(
|
||||
bars: OHLCVBar[],
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, PlotData> {
|
||||
const len = Math.max(1, Number(params.length ?? 13) || 13);
|
||||
const maLen = Math.max(1, Number(params.maLength ?? 10) || 10);
|
||||
const rawMa = String(params.maType ?? 'SMA');
|
||||
const maType = rawMa === 'EMA' || rawMa === 'WMA' ? rawMa : (rawMa === 'None' ? 'None' : 'SMA');
|
||||
const srcKey = String(params.src ?? 'close');
|
||||
const prices = bars.map(b => sourceAt(b, srcKey));
|
||||
|
||||
const cci = prices.map((_, i) => {
|
||||
if (i < len - 1) return NaN;
|
||||
let sum = 0;
|
||||
for (let j = i - len + 1; j <= i; j++) sum += prices[j];
|
||||
const ma = sum / len;
|
||||
let md = 0;
|
||||
for (let j = i - len + 1; j <= i; j++) md += Math.abs(prices[j] - ma);
|
||||
md /= len;
|
||||
if (md === 0) return 0;
|
||||
return (prices[i] - ma) / (0.015 * md);
|
||||
});
|
||||
|
||||
let signal: number[];
|
||||
if (maType === 'None') {
|
||||
signal = cci.map(() => NaN);
|
||||
} else if (maType === 'EMA') {
|
||||
signal = ema(cci.map(v => (Number.isFinite(v) ? v : NaN)), maLen);
|
||||
} else if (maType === 'WMA') {
|
||||
signal = wma(cci.map(v => (Number.isFinite(v) ? v : NaN)), maLen);
|
||||
} else {
|
||||
signal = sma(cci, maLen);
|
||||
}
|
||||
|
||||
return {
|
||||
plot0: toPlotData(bars, cci),
|
||||
plot1: toPlotData(bars, signal),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TRIX + TRMA(시그널) — 업비트·키움 HTS
|
||||
* EMA3 = EMA(EMA(EMA(종가, N), N), N)
|
||||
@@ -241,17 +353,7 @@ export function calcOBVWithMA(
|
||||
ma = ema(obv, maLen);
|
||||
break;
|
||||
case 'WMA':
|
||||
ma = obv.map((_, i) => {
|
||||
if (i < maLen - 1) return NaN;
|
||||
let wSum = 0;
|
||||
let w = 0;
|
||||
for (let j = 0; j < maLen; j++) {
|
||||
const weight = j + 1;
|
||||
wSum += obv[i - maLen + 1 + j] * weight;
|
||||
w += weight;
|
||||
}
|
||||
return wSum / w;
|
||||
});
|
||||
ma = wma(obv, maLen);
|
||||
break;
|
||||
default:
|
||||
ma = sma(obv, maLen);
|
||||
|
||||
@@ -97,6 +97,19 @@ export function formatChartAxisPrice(price: number): string {
|
||||
});
|
||||
}
|
||||
|
||||
/** 보조지표 pane 우측 가격축·시리즈 라벨 (소수점 표시) */
|
||||
export function formatIndicatorAxisPrice(price: number): string {
|
||||
if (!Number.isFinite(price)) return String(price);
|
||||
const abs = Math.abs(price);
|
||||
if (abs >= 1_000) {
|
||||
return price.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
||||
}
|
||||
if (abs >= 1) {
|
||||
return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 });
|
||||
}
|
||||
|
||||
export function formatTime(ts: number, timeframe: Timeframe): string {
|
||||
return formatUnixForChart(ts, timeframe);
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ export const INDICATOR_REGISTRY: IndicatorDef[] = [
|
||||
{ type:'RSI', name:'Relative Strength Index', koreanName:'상대강도지수', shortName:'RSI', category:'Oscillators', overlay:false, defaultParams:{length:14, src:'close', maType:'SMA', maLength:14}, plots:[{id:'plot0',title:'RSI',color:'#7E57C2',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#F23645',type:'line',lineWidth:1}], hlines:[{price:70,color:'#EF5350'},{price:50,color:'#607D8B'},{price:30,color:'#4CAF50'}], description:'상대 강도 지수 (0~100)' },
|
||||
{ type:'Stochastic', name:'Stochastic Slow', koreanName:'스토케스틱 슬로우', shortName:'Stoch', category:'Oscillators', overlay:false, defaultParams:{kLength:14, dSmoothing:3, smooth:3}, plots:[{id:'plot0',title:'%K',color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'%D',color:'#FF6D00',type:'line',lineWidth:1}], hlines:STOCHASTIC_DEFAULT_HLINES, description:'Stochastic Slow %K/%D (Slowing 적용)' },
|
||||
{ type:'StochRSI', name:'Stochastic RSI', koreanName:'스토캐스틱RSI', shortName:'StochRSI',category:'Oscillators',overlay:false, defaultParams:{lengthRSI:14, lengthStoch:14, smoothK:3, smoothD:3, src:'close'}, plots:[{id:'plot0',title:'%K',color:'#2196F3',type:'line',lineWidth:1},{id:'plot1',title:'%D',color:'#FF6D00',type:'line',lineWidth:1}], hlines:STOCHASTIC_DEFAULT_HLINES, description:'스토캐스틱 RSI' },
|
||||
{ type:'CCI', name:'Commodity Channel Index', koreanName:'상품채널지수', shortName:'CCI', category:'Oscillators', overlay:false, defaultParams:{length:13, src:'hlc3', maType:'SMA', maLength:20}, plots:[{id:'plot0',title:'CCI',color:'#2962FF',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#F23645',type:'line',lineWidth:1}], hlines:[{price:100,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-100,color:'#4CAF50'}], description:'상품 채널 지수' },
|
||||
{ type:'CCI', name:'Commodity Channel Index', koreanName:'상품채널지수', shortName:'CCI', category:'Oscillators', overlay:false, defaultParams:{length:13, src:'close', maType:'SMA', maLength:10}, plots:[{id:'plot0',title:'CCI',color:'#F23645',type:'line',lineWidth:2},{id:'plot1',title:'Signal',color:'#2962FF',type:'line',lineWidth:1}], hlines:[{price:100,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-100,color:'#4CAF50'}], description:'상품 채널 지수 (업비트: 종가·신호 10)' },
|
||||
{ type:'WilliamsPercentRange',name:'Williams %R', koreanName:'윌리엄스 %R', shortName:'W%R', category:'Oscillators', overlay:false, defaultParams:{length:14, src:'close'}, plots:[{id:'plot0',title:'%R', color:'#9C27B0',type:'line',lineWidth:2}], hlines:[{price:-20,color:'#EF5350'},{price:-50,color:'#607D8B'},{price:-80,color:'#4CAF50'}], description:'윌리엄스 %R 오실레이터' },
|
||||
{ type:'AwesomeOscillator', name:'Awesome Oscillator', koreanName:'어썸오실레이터', shortName:'AO', category:'Oscillators', overlay:false, defaultParams:{}, plots:[{id:'plot0',title:'AO',color:'#26A69A',type:'histogram'}], hlines:[{price:0,color:'#607D8B'}], description:'AO 오실레이터' },
|
||||
{ type:'ChandeMO', name:'Chande Momentum Oscillator', koreanName:'챈드모멘텀오실레이터', shortName:'CMO', category:'Oscillators', overlay:false, defaultParams:{length:9, src:'close'}, plots:[{id:'plot0',title:'CMO',color:'#4DB6AC',type:'line',lineWidth:2}], hlines:[{price:50,color:'#EF5350'},{price:0,color:'#607D8B'},{price:-50,color:'#4CAF50'}], description:'챈드 모멘텀 오실레이터' },
|
||||
@@ -454,6 +454,18 @@ export function normalizeObvParams(
|
||||
return { ...params, maType, maLength };
|
||||
}
|
||||
|
||||
/** 업비트 CCI: 기간 13·종가·신호 SMA 10 */
|
||||
export function normalizeCciParams(
|
||||
params: Record<string, number | string | boolean>,
|
||||
): Record<string, number | string | boolean> {
|
||||
const rawMa = String(params.maType ?? 'SMA');
|
||||
const maType = rawMa === 'EMA' || rawMa === 'WMA' || rawMa === 'None' ? rawMa : 'SMA';
|
||||
const src = String(params.src ?? 'close');
|
||||
const length = Math.max(1, Number(params.length ?? 13) || 13);
|
||||
const maLength = Math.max(1, Number(params.maLength ?? 10) || 10);
|
||||
return { ...params, src, maType, length, maLength };
|
||||
}
|
||||
|
||||
/** 차트·설정 모달용 — 파라미터·플롯·가시성을 registry 기본값과 병합 */
|
||||
export function enrichIndicatorConfig(
|
||||
cfg: import('../types').IndicatorConfig
|
||||
@@ -492,6 +504,9 @@ export function enrichIndicatorConfig(
|
||||
if (cfgNorm.type === 'OBV') {
|
||||
params = normalizeObvParams(params);
|
||||
}
|
||||
if (cfgNorm.type === 'CCI') {
|
||||
params = normalizeCciParams(params);
|
||||
}
|
||||
if (cfgNorm.type === 'Psychological' || cfgNorm.type === 'NewPsychological' || cfgNorm.type === 'InvestPsychological') {
|
||||
params = normalizePsychologicalParams(cfgNorm.type, params);
|
||||
}
|
||||
@@ -592,6 +607,8 @@ async function loadCustomCalculators() {
|
||||
CUSTOM_CALCULATORS.InvestPsychological = c.calcInvestPsychological;
|
||||
CUSTOM_CALCULATORS.BollingerBands = c.calcBollingerBands;
|
||||
CUSTOM_CALCULATORS.DMI = c.calcDMI;
|
||||
CUSTOM_CALCULATORS.CCI = c.calcCCI;
|
||||
CUSTOM_CALCULATORS.RSI = c.calcRSI;
|
||||
}
|
||||
|
||||
/** 차트 심볼·타임프레임 (BB 다른 심볼용) */
|
||||
|
||||
Reference in New Issue
Block a user