cci 수정

This commit is contained in:
Macbook
2026-05-30 00:46:31 +09:00
parent ad92dace28
commit 09e44008cb
7 changed files with 107 additions and 25 deletions
@@ -7,12 +7,34 @@ import org.ta4j.core.indicators.averages.SMAIndicator;
import org.ta4j.core.indicators.statistics.MeanDeviationIndicator; import org.ta4j.core.indicators.statistics.MeanDeviationIndicator;
import org.ta4j.core.num.Num; import org.ta4j.core.num.Num;
import java.util.HashMap;
import java.util.Map;
/** /**
* TradingView/업비트 CCI — 임의 가격 소스에 대해 * TradingView/업비트 CCI — 임의 가격 소스에 대해
* (src - SMA) / (0.015 × MeanDeviation) 계산. * (src - SMA) / (0.015 × MeanDeviation) 계산.
*/ */
public class CciOnSourceIndicator extends CachedIndicator<Num> { public class CciOnSourceIndicator extends CachedIndicator<Num> {
/** 업비트·TradingView 기본: hlc3(고저종), 기간 13, 신호 SMA 10 */
public static Map<String, Object> normalizeParams(Map<String, Object> p) {
Map<String, Object> out = new HashMap<>(p);
String maType = String.valueOf(p.getOrDefault("maType", "SMA"));
int length = intParam(p, "length", 13);
int maLength = intParam(p, "maLength", 10);
String src = String.valueOf(p.getOrDefault("src", "hlc3"));
if ("close".equals(src) && length == 13 && maLength == 10 && "SMA".equals(maType)) {
src = "hlc3";
}
out.put("src", src);
return out;
}
private static int intParam(Map<String, Object> p, String k, int def) {
Object v = p.get(k);
return v == null ? def : ((Number) v).intValue();
}
private final Indicator<Num> price; private final Indicator<Num> price;
private final SMAIndicator sma; private final SMAIndicator sma;
private final MeanDeviationIndicator meanDeviation; private final MeanDeviationIndicator meanDeviation;
@@ -398,8 +398,9 @@ public class IndicatorService {
int len = intP(p, "length", 13); int len = intP(p, "length", 13);
int maLen = intP(p, "maLength", 10); int maLen = intP(p, "maLength", 10);
String maType = p.getOrDefault("maType", "SMA").toString(); String maType = p.getOrDefault("maType", "SMA").toString();
Map<String, Object> norm = CciOnSourceIndicator.normalizeParams(p);
CciOnSourceIndicator cci = new CciOnSourceIndicator(src(s, p), len); CciOnSourceIndicator cci = new CciOnSourceIndicator(src(s, norm), len);
Indicator<Num> maInd = switch (maType) { Indicator<Num> maInd = switch (maType) {
case "EMA" -> new EMAIndicator(cci, maLen); case "EMA" -> new EMAIndicator(cci, maLen);
@@ -599,10 +599,10 @@ public class StrategyDslToTa4jAdapter {
// CCI — 기간 접미사(CCI_VALUE_13) 또는 기본 CCI_VALUE // CCI — 기간 접미사(CCI_VALUE_13) 또는 기본 CCI_VALUE
if (field.startsWith("CCI_VALUE_")) { if (field.startsWith("CCI_VALUE_")) {
int period = parseTrailingInt(field, "CCI_VALUE_", intP(p, "length", 13)); int period = parseTrailingInt(field, "CCI_VALUE_", intP(p, "length", 13));
return new CciOnSourceIndicator(resolvePriceSource(s, p), period); return new CciOnSourceIndicator(resolvePriceSource(s, CciOnSourceIndicator.normalizeParams(p)), period);
} }
if (field.equals("CCI_VALUE") || indType.equals("CCI")) if (field.equals("CCI_VALUE") || indType.equals("CCI"))
return new CciOnSourceIndicator(resolvePriceSource(s, p), return new CciOnSourceIndicator(resolvePriceSource(s, CciOnSourceIndicator.normalizeParams(p)),
periodOverride > 0 ? periodOverride : intP(p, "length", 13)); periodOverride > 0 ? periodOverride : intP(p, "length", 13));
// RSI — 기간 접미사(RSI_VALUE_9) 또는 기본 RSI_VALUE // RSI — 기간 접미사(RSI_VALUE_9) 또는 기본 RSI_VALUE
if (field.startsWith("RSI_VALUE_")) { if (field.startsWith("RSI_VALUE_")) {
+48 -1
View File
@@ -9929,7 +9929,54 @@ html.theme-blue {
overflow: hidden; overflow: hidden;
} }
/* 매매 탭: 매수·매도 카드 분리 */ /* 실시간 차트 우측 — 알림목록·가상매매와 동일 PaperSplitPanel 레이아웃 */
.rsp-body.ptd-right-body {
padding: 0;
gap: 0;
}
.rsp-body .ptd-split-panel--right {
flex: 1;
min-height: 0;
height: 100%;
padding: 8px 10px 6px;
box-sizing: border-box;
}
.rsp-body .ptd-ob-stack--fill {
flex: 1;
min-height: 0;
height: 100%;
}
.rsp-body .ptd-split-panel--right .ptd-order-card .top-actions {
margin-top: auto;
}
.rsp-tabs.bps-right-tabs {
display: flex;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
background: var(--bg3);
min-width: 420px;
}
.rsp-tabs.bps-right-tabs .bps-right-tab {
flex: 1;
padding: 10px 6px;
border: none;
background: transparent;
color: var(--text3);
font-size: 13px;
cursor: pointer;
transition: color 0.12s;
}
.rsp-tabs.bps-right-tabs .bps-right-tab:hover {
color: var(--text);
}
.rsp-tabs.bps-right-tabs .bps-right-tab--on {
color: var(--accent);
box-shadow: inset 0 -2px 0 var(--accent);
font-weight: 600;
}
/* 매매 탭: 매수·매도 카드 분리 (레거시 rsp-trade-stack — 호가·백테스트 등에서 사용) */
.rsp-trade-stack { .rsp-trade-stack {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
+16 -15
View File
@@ -6,6 +6,7 @@
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { OrderbookPanel } from './OrderbookPanel'; import { OrderbookPanel } from './OrderbookPanel';
import TradeOrderPanel from './TradeOrderPanel'; import TradeOrderPanel from './TradeOrderPanel';
import PaperSplitPanel from './paper/PaperSplitPanel';
import type { OrderbookDisplayUnit, WsStatus } from '../hooks/useUpbitOrderbook'; import type { OrderbookDisplayUnit, WsStatus } from '../hooks/useUpbitOrderbook';
import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades'; import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
import type { OrderbookTickerInfo } from './OrderbookPanel'; import type { OrderbookTickerInfo } from './OrderbookPanel';
@@ -130,12 +131,12 @@ const RightSidePanel: React.FC<RightSidePanelProps> = ({
</button> </button>
<div className="rsp-panel"> <div className="rsp-panel">
<div className="rsp-tabs"> <div className="rsp-tabs bps-right-tabs">
{PANEL_TABS.map(tab => ( {PANEL_TABS.map(tab => (
<button <button
key={tab.id} key={tab.id}
type="button" type="button"
className={`rsp-tab ${activeTab === tab.id ? 'rsp-tab--active' : ''}`} className={`bps-right-tab${activeTab === tab.id ? ' bps-right-tab--on' : ''}`}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
> >
{tab.label} {tab.label}
@@ -143,12 +144,14 @@ const RightSidePanel: React.FC<RightSidePanelProps> = ({
))} ))}
</div> </div>
<div className="rsp-body"> <div className="rsp-body ptd-right-body" ref={tradeSectionRef}>
{activeTab === 'trade' && ( {activeTab === 'trade' && (
<div className="rsp-trade-stack" ref={tradeSectionRef}> <PaperSplitPanel
<div className="rsp-trade-card rsp-trade-card--buy"> className="ptd-split-panel--right"
<div className="rsp-trade-card-title"></div> topTitle="매수"
<div className="rsp-trade-card-body"> bottomTitle="매도"
top={(
<div className="ptd-order-card">
<TradeOrderPanel <TradeOrderPanel
side="buy" side="buy"
market={market} market={market}
@@ -163,11 +166,9 @@ const RightSidePanel: React.FC<RightSidePanelProps> = ({
onPaperOrderFilled={onPaperOrderFilled} onPaperOrderFilled={onPaperOrderFilled}
/> />
</div> </div>
</div> )}
bottom={(
<div className="rsp-trade-card rsp-trade-card--sell"> <div className="ptd-order-card">
<div className="rsp-trade-card-title"></div>
<div className="rsp-trade-card-body">
<TradeOrderPanel <TradeOrderPanel
side="sell" side="sell"
market={market} market={market}
@@ -183,12 +184,12 @@ const RightSidePanel: React.FC<RightSidePanelProps> = ({
onPaperOrderFilled={onPaperOrderFilled} onPaperOrderFilled={onPaperOrderFilled}
/> />
</div> </div>
</div> )}
</div> />
)} )}
{activeTab === 'orderbook' && ( {activeTab === 'orderbook' && (
<div className="rsp-ob-stack"> <div className="rsp-ob-stack ptd-ob-stack--fill">
<div className="rsp-trade-card rsp-ob-card"> <div className="rsp-trade-card rsp-ob-card">
<div className="rsp-trade-card-title rsp-ob-card-title"> </div> <div className="rsp-trade-card-title rsp-ob-card-title"> </div>
<div className="rsp-trade-card-body rsp-ob-card-body"> <div className="rsp-trade-card-body rsp-ob-card-body">
+7 -3
View File
@@ -10,7 +10,11 @@ function sma(values: number[], period: number): number[] {
return values.map((_, i) => { return values.map((_, i) => {
if (i < period - 1) return NaN; if (i < period - 1) return NaN;
let sum = 0; let sum = 0;
for (let j = i - period + 1; j <= i; j++) sum += values[j]; for (let j = i - period + 1; j <= i; j++) {
const v = values[j];
if (!Number.isFinite(v)) return NaN;
sum += v;
}
return sum / period; return sum / period;
}); });
} }
@@ -252,7 +256,7 @@ export function calcRSI(
/** /**
* CCI + 신호선 (업비트·TradingView) * CCI + 신호선 (업비트·TradingView)
* CCI = (src - SMA(src,N)) / (0.015 × MeanDeviation) * CCI = (src - SMA(src,N)) / (0.015 × MeanDeviation)
* 신호선 = CCI의 SMA/EMA/WMA (기본 SMA 10) * src 기본 hlc3(고저종 평균), 신호선 = CCI의 SMA/EMA/WMA (기본 SMA 10)
*/ */
export function calcCCI( export function calcCCI(
bars: OHLCVBar[], bars: OHLCVBar[],
@@ -262,7 +266,7 @@ export function calcCCI(
const maLen = Math.max(1, Number(params.maLength ?? 10) || 10); const maLen = Math.max(1, Number(params.maLength ?? 10) || 10);
const rawMa = String(params.maType ?? 'SMA'); const rawMa = String(params.maType ?? 'SMA');
const maType = rawMa === 'EMA' || rawMa === 'WMA' ? rawMa : (rawMa === 'None' ? 'None' : 'SMA'); const maType = rawMa === 'EMA' || rawMa === 'WMA' ? rawMa : (rawMa === 'None' ? 'None' : 'SMA');
const srcKey = String(params.src ?? 'close'); const srcKey = String(params.src ?? 'hlc3');
const prices = bars.map(b => sourceAt(b, srcKey)); const prices = bars.map(b => sourceAt(b, srcKey));
const cci = prices.map((_, i) => { const cci = prices.map((_, i) => {
+10 -3
View File
@@ -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:'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:'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:'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:'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:'CCI', name:'Commodity Channel Index', koreanName:'상품채널지수', shortName:'CCI', category:'Oscillators', overlay:false, defaultParams:{length:13, src:'hlc3', 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:'상품 채널 지수 (업비트·TradingView: hlc3·신호 SMA 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:'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:'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:'챈드 모멘텀 오실레이터' }, { 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,15 +454,19 @@ export function normalizeObvParams(
return { ...params, maType, maLength }; return { ...params, maType, maLength };
} }
/** 업비트 CCI: 기간 13·종가·신호 SMA 10 */ /** 업비트·TradingView CCI: 기간 13·hlc3(고저종)·신호 SMA 10 */
export function normalizeCciParams( export function normalizeCciParams(
params: Record<string, number | string | boolean>, params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> { ): Record<string, number | string | boolean> {
const rawMa = String(params.maType ?? 'SMA'); const rawMa = String(params.maType ?? 'SMA');
const maType = rawMa === 'EMA' || rawMa === 'WMA' || rawMa === 'None' ? rawMa : 'SMA'; const maType = rawMa === 'EMA' || rawMa === 'WMA' || rawMa === 'None' ? rawMa : 'SMA';
const src = String(params.src ?? 'close'); let src = String(params.src ?? 'hlc3');
const length = Math.max(1, Number(params.length ?? 13) || 13); const length = Math.max(1, Number(params.length ?? 13) || 13);
const maLength = Math.max(1, Number(params.maLength ?? 10) || 10); const maLength = Math.max(1, Number(params.maLength ?? 10) || 10);
// 레거시: 잘못 저장된 close 기본값 → 업비트 동일 hlc3
if (src === 'close' && length === 13 && maLength === 10 && maType === 'SMA') {
src = 'hlc3';
}
return { ...params, src, maType, length, maLength }; return { ...params, src, maType, length, maLength };
} }
@@ -629,6 +633,9 @@ export async function calculateIndicator(
let calcBars = bars; let calcBars = bars;
let calcParams = params; let calcParams = params;
if (type === 'CCI') {
calcParams = normalizeCciParams(params);
}
if (type === 'IchimokuCloud') { if (type === 'IchimokuCloud') {
const { senkou } = resolveIchimokuDisplacements(params); const { senkou } = resolveIchimokuDisplacements(params);
calcParams = { ...params, displacement: senkou }; calcParams = { ...params, displacement: senkou };