dipping-charts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/__tests__/FullFeaturedChart.test.d.ts +2 -0
- package/dist/__tests__/FullFeaturedChart.test.d.ts.map +1 -0
- package/dist/__tests__/indicators-accuracy.test.d.ts +2 -0
- package/dist/__tests__/indicators-accuracy.test.d.ts.map +1 -0
- package/dist/__tests__/indicators.test.d.ts +2 -0
- package/dist/__tests__/indicators.test.d.ts.map +1 -0
- package/dist/__tests__/setup.d.ts +1 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/validateCandle.test.d.ts +2 -0
- package/dist/__tests__/validateCandle.test.d.ts.map +1 -0
- package/dist/chart/index.d.ts +2 -0
- package/dist/chart/index.js +5 -0
- package/dist/chart/index.js.map +1 -0
- package/dist/components/TradingChart.d.ts +24 -0
- package/dist/components/TradingChart.d.ts.map +1 -0
- package/dist/components/TradingChart.js +100 -0
- package/dist/components/TradingChart.js.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/dipping-charts.css +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/indicators/atr.d.ts +15 -0
- package/dist/indicators/atr.d.ts.map +1 -0
- package/dist/indicators/atr.js +30 -0
- package/dist/indicators/atr.js.map +1 -0
- package/dist/indicators/bollingerBands.d.ts +11 -0
- package/dist/indicators/bollingerBands.d.ts.map +1 -0
- package/dist/indicators/bollingerBands.js +39 -0
- package/dist/indicators/bollingerBands.js.map +1 -0
- package/dist/indicators/currencyStrength.d.ts +43 -0
- package/dist/indicators/currencyStrength.d.ts.map +1 -0
- package/dist/indicators/currencyStrength.js +53 -0
- package/dist/indicators/currencyStrength.js.map +1 -0
- package/dist/indicators/ema.d.ts +11 -0
- package/dist/indicators/ema.d.ts.map +1 -0
- package/dist/indicators/ema.js +24 -0
- package/dist/indicators/ema.js.map +1 -0
- package/dist/indicators/index.d.ts +19 -0
- package/dist/indicators/index.d.ts.map +1 -0
- package/dist/indicators/index.js +23 -0
- package/dist/indicators/index.js.map +1 -0
- package/dist/indicators/macd.d.ts +11 -0
- package/dist/indicators/macd.d.ts.map +1 -0
- package/dist/indicators/macd.js +52 -0
- package/dist/indicators/macd.js.map +1 -0
- package/dist/indicators/rsi.d.ts +11 -0
- package/dist/indicators/rsi.d.ts.map +1 -0
- package/dist/indicators/rsi.js +29 -0
- package/dist/indicators/rsi.js.map +1 -0
- package/dist/indicators/sma.d.ts +13 -0
- package/dist/indicators/sma.d.ts.map +1 -0
- package/dist/indicators/sma.js +22 -0
- package/dist/indicators/sma.js.map +1 -0
- package/dist/indicators/stochastic.d.ts +15 -0
- package/dist/indicators/stochastic.d.ts.map +1 -0
- package/dist/indicators/stochastic.js +34 -0
- package/dist/indicators/stochastic.js.map +1 -0
- package/dist/indicators/types.d.ts +102 -0
- package/dist/indicators/types.d.ts.map +1 -0
- package/dist/indicators/vwap.d.ts +14 -0
- package/dist/indicators/vwap.d.ts.map +1 -0
- package/dist/indicators/vwap.js +17 -0
- package/dist/indicators/vwap.js.map +1 -0
- package/dist/indicators/williamsR.d.ts +17 -0
- package/dist/indicators/williamsR.d.ts.map +1 -0
- package/dist/indicators/williamsR.js +19 -0
- package/dist/indicators/williamsR.js.map +1 -0
- package/dist/react/FullFeaturedChart.d.ts +3 -0
- package/dist/react/FullFeaturedChart.d.ts.map +1 -0
- package/dist/react/FullFeaturedChart.js +640 -0
- package/dist/react/FullFeaturedChart.js.map +1 -0
- package/dist/react/components/IndicatorSettings.d.ts +20 -0
- package/dist/react/components/IndicatorSettings.d.ts.map +1 -0
- package/dist/react/components/IndicatorSettings.js +748 -0
- package/dist/react/components/IndicatorSettings.js.map +1 -0
- package/dist/react/hooks/useChart.d.ts +15 -0
- package/dist/react/hooks/useChart.d.ts.map +1 -0
- package/dist/react/hooks/useChart.js +155 -0
- package/dist/react/hooks/useChart.js.map +1 -0
- package/dist/react/hooks/useIndicators.d.ts +10 -0
- package/dist/react/hooks/useIndicators.d.ts.map +1 -0
- package/dist/react/hooks/useIndicators.js +264 -0
- package/dist/react/hooks/useIndicators.js.map +1 -0
- package/dist/react/hooks/useLineTools.d.ts +26 -0
- package/dist/react/hooks/useLineTools.d.ts.map +1 -0
- package/dist/react/hooks/useLineTools.js +189 -0
- package/dist/react/hooks/useLineTools.js.map +1 -0
- package/dist/react/hooks/useShiftSnap.d.ts +12 -0
- package/dist/react/hooks/useShiftSnap.d.ts.map +1 -0
- package/dist/react/hooks/useShiftSnap.js +54 -0
- package/dist/react/hooks/useShiftSnap.js.map +1 -0
- package/dist/react/index.d.ts +14 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +18 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/loadLightweightCharts.d.ts +18 -0
- package/dist/react/loadLightweightCharts.d.ts.map +1 -0
- package/dist/react/loadLightweightCharts.js +32 -0
- package/dist/react/loadLightweightCharts.js.map +1 -0
- package/dist/react/locale.d.ts +79 -0
- package/dist/react/locale.d.ts.map +1 -0
- package/dist/react/locale.js +158 -0
- package/dist/react/locale.js.map +1 -0
- package/dist/react/types.d.ts +130 -0
- package/dist/react/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/getToolId.d.ts +9 -0
- package/dist/utils/getToolId.d.ts.map +1 -0
- package/dist/utils/getToolId.js +12 -0
- package/dist/utils/getToolId.js.map +1 -0
- package/dist/utils/mockData.d.ts +10 -0
- package/dist/utils/mockData.d.ts.map +1 -0
- package/dist/utils/mockData.js +61 -0
- package/dist/utils/mockData.js.map +1 -0
- package/dist/utils/snapCrosshair.d.ts +25 -0
- package/dist/utils/snapCrosshair.d.ts.map +1 -0
- package/dist/utils/validateCandle.d.ts +30 -0
- package/dist/utils/validateCandle.d.ts.map +1 -0
- package/dist/utils/validateCandle.js +21 -0
- package/dist/utils/validateCandle.js.map +1 -0
- package/examples/css/base.css +209 -0
- package/examples/css/chart.css +282 -0
- package/examples/css/indicators.css +255 -0
- package/examples/index.html +163 -0
- package/examples/js/chart.js +370 -0
- package/examples/js/indicators.js +27 -0
- package/examples/js/main.js +6 -0
- package/examples/js/ui.js +1641 -0
- package/lib/lightweight-charts.standalone.production.js +7 -0
- package/package.json +106 -0
- package/src/react/FullFeaturedChart.css +1007 -0
|
@@ -0,0 +1,1641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 난독화된 lightweight-charts Line Tool 결과에서 도구 ID를 추출합니다.
|
|
3
|
+
*/
|
|
4
|
+
function getToolIdFromResult(result) {
|
|
5
|
+
try {
|
|
6
|
+
return result?.ak?.ji ?? null;
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Import from chart.js
|
|
13
|
+
import {
|
|
14
|
+
chart,
|
|
15
|
+
chartContainer,
|
|
16
|
+
candleSeries,
|
|
17
|
+
volumeSeries,
|
|
18
|
+
currentCandles,
|
|
19
|
+
indicatorSeries,
|
|
20
|
+
indicatorStates,
|
|
21
|
+
dynamicIndicatorSeries,
|
|
22
|
+
loadData,
|
|
23
|
+
toggleSMA20,
|
|
24
|
+
toggleEMA12,
|
|
25
|
+
toggleRSI14,
|
|
26
|
+
toggleMACD,
|
|
27
|
+
toggleBollingerBands
|
|
28
|
+
} from './chart.js';
|
|
29
|
+
|
|
30
|
+
// 시간봉 버튼 이벤트
|
|
31
|
+
document.querySelectorAll('.btn-timeframe').forEach(btn => {
|
|
32
|
+
btn.addEventListener('click', () => {
|
|
33
|
+
const timeFrame = btn.dataset.timeframe;
|
|
34
|
+
|
|
35
|
+
// active 클래스 토글
|
|
36
|
+
document.querySelectorAll('.btn-timeframe').forEach(b => b.classList.remove('active'));
|
|
37
|
+
btn.classList.add('active');
|
|
38
|
+
|
|
39
|
+
// 데이터 로드
|
|
40
|
+
loadData(timeFrame);
|
|
41
|
+
|
|
42
|
+
// 보조지표 다시 그리기
|
|
43
|
+
if (typeof applyAllIndicators === 'function') {
|
|
44
|
+
applyAllIndicators();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// 반응형
|
|
50
|
+
window.addEventListener('resize', () => {
|
|
51
|
+
chart.applyOptions({
|
|
52
|
+
width: chartContainer.clientWidth,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ===== Snap Crosshair Plugin =====
|
|
57
|
+
let isShiftPressed = false;
|
|
58
|
+
|
|
59
|
+
// 커스텀 crosshair 요소 생성
|
|
60
|
+
const verticalLine = document.createElement('div');
|
|
61
|
+
verticalLine.style.cssText = 'position: absolute; width: 1px; height: 100%; top: 0; background: #2962FF; pointer-events: none; display: none; z-index: 1000;';
|
|
62
|
+
chartContainer.appendChild(verticalLine);
|
|
63
|
+
|
|
64
|
+
const horizontalLine = document.createElement('div');
|
|
65
|
+
horizontalLine.style.cssText = 'position: absolute; height: 1px; width: 100%; left: 0; background: #2962FF; pointer-events: none; display: none; z-index: 1000;';
|
|
66
|
+
chartContainer.appendChild(horizontalLine);
|
|
67
|
+
|
|
68
|
+
const priceLabel = document.createElement('div');
|
|
69
|
+
priceLabel.style.cssText = 'position: absolute; background: #2962FF; color: white; padding: 4px 8px; font-size: 11px; font-family: monospace; pointer-events: none; display: none; z-index: 1001; transform: translateY(-50%);';
|
|
70
|
+
chartContainer.appendChild(priceLabel);
|
|
71
|
+
|
|
72
|
+
// Shift 키 이벤트
|
|
73
|
+
window.addEventListener('keydown', (e) => {
|
|
74
|
+
if (e.key === 'Shift' && !isShiftPressed) {
|
|
75
|
+
isShiftPressed = true;
|
|
76
|
+
// 기본 crosshair 숨기기
|
|
77
|
+
chart.applyOptions({
|
|
78
|
+
crosshair: {
|
|
79
|
+
vertLine: { visible: false },
|
|
80
|
+
horzLine: { visible: false },
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
window.addEventListener('keyup', (e) => {
|
|
87
|
+
if (e.key === 'Shift') {
|
|
88
|
+
isShiftPressed = false;
|
|
89
|
+
// 기본 crosshair 복원
|
|
90
|
+
chart.applyOptions({
|
|
91
|
+
crosshair: {
|
|
92
|
+
vertLine: { visible: true },
|
|
93
|
+
horzLine: { visible: true },
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
// 커스텀 crosshair 숨기기
|
|
97
|
+
verticalLine.style.display = 'none';
|
|
98
|
+
horizontalLine.style.display = 'none';
|
|
99
|
+
priceLabel.style.display = 'none';
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Crosshair 이동 감지
|
|
104
|
+
chart.subscribeCrosshairMove((param) => {
|
|
105
|
+
if (!isShiftPressed || !param.point || !param.time) {
|
|
106
|
+
if (isShiftPressed) {
|
|
107
|
+
verticalLine.style.display = 'none';
|
|
108
|
+
horizontalLine.style.display = 'none';
|
|
109
|
+
priceLabel.style.display = 'none';
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 현재 캔들 데이터
|
|
115
|
+
const candleData = param.seriesData.get(candleSeries);
|
|
116
|
+
if (!candleData) return;
|
|
117
|
+
|
|
118
|
+
const high = candleData.high;
|
|
119
|
+
const low = candleData.low;
|
|
120
|
+
|
|
121
|
+
// coordinateToPrice로 마우스 Y 좌표를 가격으로 변환 (더 정확함)
|
|
122
|
+
const mousePrice = candleSeries.coordinateToPrice(param.point.y);
|
|
123
|
+
if (mousePrice === null) return;
|
|
124
|
+
|
|
125
|
+
// 마우스 가격이 high와 low 중 어디에 더 가까운지 비교
|
|
126
|
+
const distToHigh = Math.abs(mousePrice - high);
|
|
127
|
+
const distToLow = Math.abs(mousePrice - low);
|
|
128
|
+
|
|
129
|
+
// 더 가까운 가격에 snap
|
|
130
|
+
const snapPrice = distToHigh < distToLow ? high : low;
|
|
131
|
+
const snapY = candleSeries.priceToCoordinate(snapPrice);
|
|
132
|
+
|
|
133
|
+
if (snapY === null) return;
|
|
134
|
+
|
|
135
|
+
// 커스텀 crosshair 표시
|
|
136
|
+
verticalLine.style.display = 'block';
|
|
137
|
+
verticalLine.style.left = param.point.x + 'px';
|
|
138
|
+
|
|
139
|
+
horizontalLine.style.display = 'block';
|
|
140
|
+
horizontalLine.style.top = snapY + 'px';
|
|
141
|
+
|
|
142
|
+
// 가격 라벨 - 오른쪽 가격 축에 배치
|
|
143
|
+
priceLabel.style.display = 'block';
|
|
144
|
+
priceLabel.style.top = snapY + 'px';
|
|
145
|
+
priceLabel.style.right = '0px';
|
|
146
|
+
priceLabel.textContent = snapPrice.toFixed(2);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ===== Line Tools =====
|
|
150
|
+
|
|
151
|
+
// Line Tools 버튼 이벤트
|
|
152
|
+
function activateLineTool(toolType, btn) {
|
|
153
|
+
// 모든 Line Tool 버튼 비활성화
|
|
154
|
+
document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
|
|
155
|
+
b.classList.remove('active');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 이미 활성화된 도구를 다시 클릭하면 비활성화
|
|
159
|
+
if (activeLineTool === toolType) {
|
|
160
|
+
activeLineTool = null;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 새 도구 활성화
|
|
165
|
+
activeLineTool = toolType;
|
|
166
|
+
btn.classList.add('active');
|
|
167
|
+
|
|
168
|
+
// 마지막 설정값으로 Line Tool 추가
|
|
169
|
+
chart.addLineTool(toolType, [], {
|
|
170
|
+
line: {
|
|
171
|
+
width: lastToolOptions.lineWidth,
|
|
172
|
+
color: lastToolOptions.lineColor
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
document.getElementById('trendLineToolBtn').addEventListener('click', (e) => {
|
|
178
|
+
activateLineTool('TrendLine', e.target);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
document.getElementById('horizontalLineToolBtn').addEventListener('click', (e) => {
|
|
182
|
+
activateLineTool('HorizontalLine', e.target);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
document.getElementById('verticalLineToolBtn').addEventListener('click', (e) => {
|
|
186
|
+
activateLineTool('VerticalLine', e.target);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
document.getElementById('rectangleToolBtn').addEventListener('click', (e) => {
|
|
190
|
+
activateLineTool('Rectangle', e.target);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
document.getElementById('fibRetracementToolBtn').addEventListener('click', (e) => {
|
|
194
|
+
activateLineTool('FibRetracement', e.target);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
document.getElementById('textToolBtn').addEventListener('click', (e) => {
|
|
198
|
+
// 모든 Line Tool 버튼 비활성화
|
|
199
|
+
document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
|
|
200
|
+
b.classList.remove('active');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 이미 활성화된 도구를 다시 클릭하면 비활성화
|
|
204
|
+
if (activeLineTool === 'Text') {
|
|
205
|
+
activeLineTool = null;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 새 도구 활성화
|
|
210
|
+
activeLineTool = 'Text';
|
|
211
|
+
e.target.classList.add('active');
|
|
212
|
+
|
|
213
|
+
// 텍스트 입력 받기 (커스텀 모달 사용)
|
|
214
|
+
showTextModal('Label').then((text) => {
|
|
215
|
+
if (text !== null && text !== '') {
|
|
216
|
+
// 마지막 설정값으로 Text 도구 추가
|
|
217
|
+
chart.addLineTool('Text', [], {
|
|
218
|
+
text: {
|
|
219
|
+
value: text,
|
|
220
|
+
font: {
|
|
221
|
+
color: lastToolOptions.lineColor,
|
|
222
|
+
size: lastToolOptions.lineWidth * 10 // 선 두께에 비례한 텍스트 크기
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
// 취소하면 버튼 비활성화
|
|
228
|
+
activeLineTool = null;
|
|
229
|
+
e.target.classList.remove('active');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// 모든 Line Tools 삭제
|
|
235
|
+
document.getElementById('removeAllLineToolsBtn').addEventListener('click', () => {
|
|
236
|
+
chart.removeAllLineTools();
|
|
237
|
+
lineToolsMap.clear(); // Map 초기화
|
|
238
|
+
selectedLineToolId = null;
|
|
239
|
+
activeLineTool = null;
|
|
240
|
+
// 모든 버튼 비활성화
|
|
241
|
+
document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
|
|
242
|
+
b.classList.remove('active');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
// ===== Context Menu for Line Tools =====
|
|
248
|
+
const contextMenu = document.getElementById('contextMenu');
|
|
249
|
+
const widthDisplay = document.getElementById('widthDisplay');
|
|
250
|
+
const colorCurrentBtn = document.getElementById('colorCurrentBtn');
|
|
251
|
+
const colorCurrentDisplay = document.getElementById('colorCurrentDisplay');
|
|
252
|
+
const colorPalette = document.getElementById('colorPalette');
|
|
253
|
+
|
|
254
|
+
// Line Tools 저장소 (직접 관리)
|
|
255
|
+
export let lineToolsMap = new Map(); // lineToolId -> { id, toolType, points, options }
|
|
256
|
+
export let selectedLineToolId = null;
|
|
257
|
+
|
|
258
|
+
// 마지막 설정값 저장 (새 도구에 적용)
|
|
259
|
+
export const lastToolOptions = {
|
|
260
|
+
lineWidth: 2,
|
|
261
|
+
lineColor: '#2962FF'
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// 컨텍스트 메뉴 숨기기
|
|
265
|
+
function hideContextMenu() {
|
|
266
|
+
contextMenu.classList.remove('show');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 컨텍스트 메뉴 표시
|
|
270
|
+
function showContextMenu(x, y, lineToolId) {
|
|
271
|
+
console.log('📋 showContextMenu 호출:', lineToolId);
|
|
272
|
+
const lineTool = lineToolsMap.get(lineToolId);
|
|
273
|
+
if (!lineTool) {
|
|
274
|
+
console.log('❌ lineToolsMap에서 도구를 찾을 수 없음:', lineToolId);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log('✅ 도구 찾음:', lineTool.toolType);
|
|
279
|
+
selectedLineToolId = lineToolId;
|
|
280
|
+
|
|
281
|
+
// 텍스트 수정 버튼 표시/숨김
|
|
282
|
+
const editTextBtn = contextMenu.querySelector('[data-action="edit-text"]');
|
|
283
|
+
const editTextSeparator = editTextBtn.previousElementSibling;
|
|
284
|
+
if (lineTool.toolType === 'Text') {
|
|
285
|
+
editTextBtn.style.display = 'flex';
|
|
286
|
+
editTextSeparator.style.display = 'block';
|
|
287
|
+
} else {
|
|
288
|
+
editTextBtn.style.display = 'none';
|
|
289
|
+
editTextSeparator.style.display = 'none';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 현재 색상 반영
|
|
293
|
+
let currentColor = '#2962FF';
|
|
294
|
+
if (lineTool.options?.line?.color) {
|
|
295
|
+
currentColor = lineTool.options.line.color;
|
|
296
|
+
} else if (lineTool.options?.text?.font?.color) {
|
|
297
|
+
currentColor = lineTool.options.text.font.color;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 현재 색상 디스플레이 업데이트
|
|
301
|
+
colorCurrentDisplay.style.background = currentColor;
|
|
302
|
+
|
|
303
|
+
// 현재 선 두께 표시
|
|
304
|
+
const currentWidth = lineTool.options?.line?.width || 1;
|
|
305
|
+
widthDisplay.textContent = currentWidth;
|
|
306
|
+
|
|
307
|
+
// 메뉴 위치 설정
|
|
308
|
+
contextMenu.style.left = x + 'px';
|
|
309
|
+
contextMenu.style.top = y + 'px';
|
|
310
|
+
contextMenu.classList.add('show');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 차트 외부 클릭 시 메뉴 숨기기
|
|
314
|
+
document.addEventListener('click', (e) => {
|
|
315
|
+
if (!contextMenu.contains(e.target)) {
|
|
316
|
+
hideContextMenu();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ESC 키 이벤트
|
|
321
|
+
document.addEventListener('keydown', (e) => {
|
|
322
|
+
if (e.key === 'Escape') {
|
|
323
|
+
// 1. 컨텍스트 메뉴가 열려있을 때: 선택된 도구 삭제
|
|
324
|
+
if (contextMenu.classList.contains('show') && selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
|
|
325
|
+
console.log('🗑️ ESC - 선택된 도구 삭제:', selectedLineToolId);
|
|
326
|
+
// 기존 도구들을 모두 저장
|
|
327
|
+
const allTools = Array.from(lineToolsMap.values());
|
|
328
|
+
|
|
329
|
+
// 선택된 도구 제외
|
|
330
|
+
const remainingTools = allTools.filter(t => t.id !== selectedLineToolId);
|
|
331
|
+
console.log(' 남은 도구 개수:', remainingTools.length);
|
|
332
|
+
|
|
333
|
+
// 모든 Line Tools 제거
|
|
334
|
+
chart.removeAllLineTools();
|
|
335
|
+
lineToolsMap.clear();
|
|
336
|
+
|
|
337
|
+
// 남은 도구들 재생성
|
|
338
|
+
let firstNewId = null;
|
|
339
|
+
remainingTools.forEach((t, index) => {
|
|
340
|
+
const result = chart.addLineTool(t.toolType, t.points, t.options);
|
|
341
|
+
|
|
342
|
+
const newId = getToolIdFromResult(result);
|
|
343
|
+
if (newId) {
|
|
344
|
+
console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId);
|
|
345
|
+
lineToolsMap.set(newId, {
|
|
346
|
+
id: newId,
|
|
347
|
+
toolType: t.toolType,
|
|
348
|
+
points: t.points,
|
|
349
|
+
options: t.options
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// 첫 번째 재생성된 도구의 ID 저장
|
|
353
|
+
if (index === 0) {
|
|
354
|
+
firstNewId = newId;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// 남은 도구가 있다면 첫 번째 도구를 자동으로 선택
|
|
360
|
+
if (firstNewId !== null) {
|
|
361
|
+
selectedLineToolId = firstNewId;
|
|
362
|
+
console.log(' ✅ 자동 선택된 도구 ID:', selectedLineToolId);
|
|
363
|
+
} else {
|
|
364
|
+
selectedLineToolId = null;
|
|
365
|
+
console.log(' ℹ️ 남은 도구가 없음');
|
|
366
|
+
}
|
|
367
|
+
hideContextMenu();
|
|
368
|
+
}
|
|
369
|
+
// 2. 그리기 도구가 활성화되어 있을 때: 그리기 취소 (점을 찍은 후에도 취소 가능)
|
|
370
|
+
else if (activeLineTool !== null) {
|
|
371
|
+
console.log('⏹️ ESC - 그리기 취소');
|
|
372
|
+
// 진행 중인 그리기 취소 - 모든 도구 제거 후 완성된 것만 재생성
|
|
373
|
+
const completedTools = Array.from(lineToolsMap.values());
|
|
374
|
+
console.log(' 완성된 도구 개수:', completedTools.length);
|
|
375
|
+
chart.removeAllLineTools();
|
|
376
|
+
lineToolsMap.clear();
|
|
377
|
+
|
|
378
|
+
// 완성된 도구들만 다시 추가
|
|
379
|
+
completedTools.forEach(t => {
|
|
380
|
+
const result = chart.addLineTool(t.toolType, t.points, t.options);
|
|
381
|
+
const newId = getToolIdFromResult(result);
|
|
382
|
+
if (newId) {
|
|
383
|
+
console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId);
|
|
384
|
+
// 기존 맵의 ID를 새 ID로 교체
|
|
385
|
+
lineToolsMap.set(newId, {
|
|
386
|
+
id: newId,
|
|
387
|
+
toolType: t.toolType,
|
|
388
|
+
points: t.points,
|
|
389
|
+
options: t.options
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
console.log(' 재생성 완료. lineToolsMap 크기:', lineToolsMap.size);
|
|
394
|
+
|
|
395
|
+
activeLineTool = null;
|
|
396
|
+
// 모든 Line Tool 버튼 비활성화
|
|
397
|
+
document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
|
|
398
|
+
b.classList.remove('active');
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// 메뉴 드래그 기능
|
|
405
|
+
let isDraggingMenu = false;
|
|
406
|
+
let menuDragOffsetX = 0;
|
|
407
|
+
let menuDragOffsetY = 0;
|
|
408
|
+
|
|
409
|
+
contextMenu.addEventListener('mousedown', (e) => {
|
|
410
|
+
// 버튼이나 색상 팔레트 클릭 시에는 드래그 안 함
|
|
411
|
+
if (e.target.classList.contains('context-menu-btn') ||
|
|
412
|
+
e.target.classList.contains('color-current-btn') ||
|
|
413
|
+
e.target.classList.contains('color-current-display') ||
|
|
414
|
+
e.target.classList.contains('color-option') ||
|
|
415
|
+
e.target.classList.contains('width-display')) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
isDraggingMenu = true;
|
|
420
|
+
menuDragOffsetX = e.clientX - contextMenu.offsetLeft;
|
|
421
|
+
menuDragOffsetY = e.clientY - contextMenu.offsetTop;
|
|
422
|
+
contextMenu.classList.add('dragging');
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
document.addEventListener('mousemove', (e) => {
|
|
427
|
+
if (!isDraggingMenu) return;
|
|
428
|
+
|
|
429
|
+
const x = e.clientX - menuDragOffsetX;
|
|
430
|
+
const y = e.clientY - menuDragOffsetY;
|
|
431
|
+
|
|
432
|
+
contextMenu.style.left = x + 'px';
|
|
433
|
+
contextMenu.style.top = y + 'px';
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
document.addEventListener('mouseup', () => {
|
|
437
|
+
if (isDraggingMenu) {
|
|
438
|
+
isDraggingMenu = false;
|
|
439
|
+
contextMenu.classList.remove('dragging');
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// 차트에서 우클릭 이벤트 감지
|
|
444
|
+
chartContainer.addEventListener('contextmenu', (e) => {
|
|
445
|
+
e.preventDefault();
|
|
446
|
+
console.log('🖱️ 우클릭 이벤트 발생');
|
|
447
|
+
console.log(' 현재 selectedLineToolId:', selectedLineToolId);
|
|
448
|
+
console.log(' lineToolsMap.has(selectedLineToolId):', lineToolsMap.has(selectedLineToolId));
|
|
449
|
+
console.log(' lineToolsMap 전체 ID 목록:', Array.from(lineToolsMap.keys()));
|
|
450
|
+
|
|
451
|
+
if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
|
|
452
|
+
console.log('✅ 컨텍스트 메뉴 표시');
|
|
453
|
+
showContextMenu(e.clientX, e.clientY, selectedLineToolId);
|
|
454
|
+
} else if (selectedLineToolId === null && lineToolsMap.size > 0) {
|
|
455
|
+
// 선택된 도구가 없지만 lineToolsMap에 도구가 있는 경우
|
|
456
|
+
// 첫 번째 도구를 자동으로 선택
|
|
457
|
+
const firstToolId = Array.from(lineToolsMap.keys())[0];
|
|
458
|
+
console.log('⚠️ 도구가 선택되지 않음 → 첫 번째 도구 자동 선택:', firstToolId);
|
|
459
|
+
selectedLineToolId = firstToolId;
|
|
460
|
+
showContextMenu(e.clientX, e.clientY, firstToolId);
|
|
461
|
+
} else if (!lineToolsMap.has(selectedLineToolId) && lineToolsMap.size > 0) {
|
|
462
|
+
// selectedLineToolId가 있지만 lineToolsMap에 없는 경우 (ID가 변경됨)
|
|
463
|
+
const firstToolId = Array.from(lineToolsMap.keys())[0];
|
|
464
|
+
console.log('⚠️ 선택된 도구 ID가 유효하지 않음 → 첫 번째 도구 자동 선택:', firstToolId);
|
|
465
|
+
selectedLineToolId = firstToolId;
|
|
466
|
+
showContextMenu(e.clientX, e.clientY, firstToolId);
|
|
467
|
+
} else {
|
|
468
|
+
console.log('❌ 도구가 없음');
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Line Tool 이벤트 구독
|
|
473
|
+
let lastClickTime = 0;
|
|
474
|
+
let lastClickedToolId = null;
|
|
475
|
+
const DOUBLE_CLICK_THRESHOLD = 300; // 300ms
|
|
476
|
+
|
|
477
|
+
// subscribeLineToolsAfterEdit로 도구 저장 및 선택 추적
|
|
478
|
+
chart.subscribeLineToolsAfterEdit((params) => {
|
|
479
|
+
console.log('📍 LineToolsAfterEdit 이벤트:', params.stage, params);
|
|
480
|
+
const tool = params.selectedLineTool;
|
|
481
|
+
if (!tool) {
|
|
482
|
+
console.log('⚠️ tool이 없음');
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const toolId = tool.id;
|
|
487
|
+
console.log('🔧 도구 ID:', toolId, '타입:', tool.toolType, '스테이지:', params.stage);
|
|
488
|
+
|
|
489
|
+
// 모든 이벤트에서 도구 정보 저장/업데이트
|
|
490
|
+
lineToolsMap.set(toolId, {
|
|
491
|
+
id: toolId,
|
|
492
|
+
toolType: tool.toolType,
|
|
493
|
+
points: tool.points,
|
|
494
|
+
options: tool.options
|
|
495
|
+
});
|
|
496
|
+
console.log('💾 lineToolsMap 저장 완료. 현재 크기:', lineToolsMap.size);
|
|
497
|
+
console.log('📋 lineToolsMap 내용:', Array.from(lineToolsMap.keys()));
|
|
498
|
+
|
|
499
|
+
// 도구 생성 완료 시 버튼 비활성화
|
|
500
|
+
if (params.stage === 'lineToolFinished' || params.stage === 'pathFinished') {
|
|
501
|
+
activeLineTool = null;
|
|
502
|
+
document.querySelectorAll('#trendLineToolBtn, #horizontalLineToolBtn, #verticalLineToolBtn, #rectangleToolBtn, #fibRetracementToolBtn, #textToolBtn').forEach(b => {
|
|
503
|
+
b.classList.remove('active');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// 도구를 그리자마자 컨텍스트 메뉴 표시 (그린 위치 바로 아래)
|
|
507
|
+
setTimeout(() => {
|
|
508
|
+
if (lineToolsMap.has(toolId) && tool.points && tool.points.length > 0) {
|
|
509
|
+
// 마지막 포인트의 좌표를 픽셀로 변환
|
|
510
|
+
const lastPoint = tool.points[tool.points.length - 1];
|
|
511
|
+
const pixelY = candleSeries.priceToCoordinate(lastPoint.price);
|
|
512
|
+
const timeScale = chart.timeScale();
|
|
513
|
+
const pixelX = timeScale.timeToCoordinate(lastPoint.timestamp);
|
|
514
|
+
|
|
515
|
+
if (pixelX !== null && pixelY !== null) {
|
|
516
|
+
const rect = chartContainer.getBoundingClientRect();
|
|
517
|
+
// 도구 위치 바로 아래에 메뉴 표시
|
|
518
|
+
const menuX = rect.left + pixelX - 150; // 툴바 너비 절반 정도 왼쪽으로
|
|
519
|
+
const menuY = rect.top + pixelY + 10; // 도구 아래 10px
|
|
520
|
+
showContextMenu(menuX, menuY, toolId);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}, 100);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 도구 클릭/선택 시
|
|
527
|
+
selectedLineToolId = toolId;
|
|
528
|
+
console.log('✅ selectedLineToolId 업데이트:', selectedLineToolId);
|
|
529
|
+
|
|
530
|
+
// 더블 클릭 감지 (텍스트 도구에만 적용)
|
|
531
|
+
const currentTime = Date.now();
|
|
532
|
+
if (currentTime - lastClickTime < DOUBLE_CLICK_THRESHOLD &&
|
|
533
|
+
lastClickedToolId === toolId &&
|
|
534
|
+
tool.toolType === 'Text') {
|
|
535
|
+
// 텍스트 수정 다이얼로그 표시 (커스텀 모달 사용)
|
|
536
|
+
const currentText = tool.options.text?.value || '';
|
|
537
|
+
showTextModal(currentText).then((newText) => {
|
|
538
|
+
if (newText !== null && newText !== '') {
|
|
539
|
+
const updatedOptions = {
|
|
540
|
+
...tool.options,
|
|
541
|
+
text: {
|
|
542
|
+
...tool.options.text,
|
|
543
|
+
value: newText,
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// 기존 도구들을 모두 저장
|
|
548
|
+
const allTools = Array.from(lineToolsMap.values());
|
|
549
|
+
|
|
550
|
+
// 모든 Line Tools 제거
|
|
551
|
+
chart.removeAllLineTools();
|
|
552
|
+
lineToolsMap.clear();
|
|
553
|
+
|
|
554
|
+
// 업데이트된 도구 포함하여 모두 재생성
|
|
555
|
+
let lastNewId = null;
|
|
556
|
+
allTools.forEach(t => {
|
|
557
|
+
const opts = t.id === toolId ? updatedOptions : t.options;
|
|
558
|
+
const result = chart.addLineTool(t.toolType, t.points, opts);
|
|
559
|
+
|
|
560
|
+
const newId = getToolIdFromResult(result);
|
|
561
|
+
if (newId) {
|
|
562
|
+
lineToolsMap.set(newId, {
|
|
563
|
+
id: newId,
|
|
564
|
+
toolType: t.toolType,
|
|
565
|
+
points: t.points,
|
|
566
|
+
options: opts
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// 업데이트된 도구의 새 ID 저장
|
|
570
|
+
if (t.id === toolId) {
|
|
571
|
+
lastNewId = newId;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// 재생성 후 업데이트된 도구의 새 ID를 선택
|
|
577
|
+
selectedLineToolId = lastNewId;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
lastClickTime = 0; // 더블 클릭 후 리셋
|
|
581
|
+
lastClickedToolId = null;
|
|
582
|
+
} else {
|
|
583
|
+
lastClickTime = currentTime;
|
|
584
|
+
lastClickedToolId = toolId;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// 선 두께 변경 함수
|
|
589
|
+
function updateLineWidth(newWidth) {
|
|
590
|
+
console.log('📏 선 두께 변경:', newWidth);
|
|
591
|
+
if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
|
|
592
|
+
const tool = lineToolsMap.get(selectedLineToolId);
|
|
593
|
+
console.log(' 선택된 도구:', selectedLineToolId, tool.toolType);
|
|
594
|
+
|
|
595
|
+
let updatedOptions;
|
|
596
|
+
if (tool.toolType === 'Text') {
|
|
597
|
+
// 텍스트 도구의 경우 폰트 크기 업데이트
|
|
598
|
+
updatedOptions = {
|
|
599
|
+
...tool.options,
|
|
600
|
+
text: {
|
|
601
|
+
...tool.options.text,
|
|
602
|
+
font: {
|
|
603
|
+
...tool.options.text?.font,
|
|
604
|
+
size: newWidth * 10, // 선 두께에 비례한 텍스트 크기
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
} else {
|
|
609
|
+
// 일반 도구의 경우 선 두께만 업데이트
|
|
610
|
+
updatedOptions = {
|
|
611
|
+
...tool.options,
|
|
612
|
+
line: {
|
|
613
|
+
...tool.options.line,
|
|
614
|
+
width: newWidth,
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 기존 도구들을 모두 저장
|
|
620
|
+
const allTools = Array.from(lineToolsMap.values());
|
|
621
|
+
console.log(' 재생성할 도구 개수:', allTools.length);
|
|
622
|
+
|
|
623
|
+
// 모든 Line Tools 제거
|
|
624
|
+
chart.removeAllLineTools();
|
|
625
|
+
lineToolsMap.clear();
|
|
626
|
+
|
|
627
|
+
// 업데이트된 도구 포함하여 모두 재생성
|
|
628
|
+
let lastNewId = null;
|
|
629
|
+
allTools.forEach(t => {
|
|
630
|
+
const opts = t.id === selectedLineToolId ? updatedOptions : t.options;
|
|
631
|
+
const result = chart.addLineTool(t.toolType, t.points, opts);
|
|
632
|
+
|
|
633
|
+
const newId = getToolIdFromResult(result);
|
|
634
|
+
if (newId) {
|
|
635
|
+
console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId, '타입:', t.toolType);
|
|
636
|
+
lineToolsMap.set(newId, {
|
|
637
|
+
id: newId,
|
|
638
|
+
toolType: t.toolType,
|
|
639
|
+
points: t.points,
|
|
640
|
+
options: opts
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// 업데이트된 도구의 새 ID 저장
|
|
644
|
+
if (t.id === selectedLineToolId) {
|
|
645
|
+
lastNewId = newId;
|
|
646
|
+
console.log(' ✅ 선택된 도구의 새 ID:', newId);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// 재생성 후 업데이트된 도구의 새 ID를 선택
|
|
652
|
+
selectedLineToolId = lastNewId;
|
|
653
|
+
console.log(' 최종 selectedLineToolId:', selectedLineToolId);
|
|
654
|
+
console.log(' 최종 lineToolsMap 크기:', lineToolsMap.size);
|
|
655
|
+
|
|
656
|
+
// 마지막 설정값에 저장 (새 도구에 적용)
|
|
657
|
+
lastToolOptions.lineWidth = newWidth;
|
|
658
|
+
|
|
659
|
+
// 두께 표시 업데이트
|
|
660
|
+
widthDisplay.textContent = newWidth;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// 선 두께 감소 버튼
|
|
665
|
+
contextMenu.querySelector('[data-action="decrease-width"]').addEventListener('click', (e) => {
|
|
666
|
+
e.stopPropagation();
|
|
667
|
+
if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
|
|
668
|
+
const tool = lineToolsMap.get(selectedLineToolId);
|
|
669
|
+
const currentWidth = tool.options?.line?.width || 1;
|
|
670
|
+
const newWidth = Math.max(1, currentWidth - 1); // 최소 1
|
|
671
|
+
updateLineWidth(newWidth);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// 선 두께 증가 버튼
|
|
676
|
+
contextMenu.querySelector('[data-action="increase-width"]').addEventListener('click', (e) => {
|
|
677
|
+
e.stopPropagation();
|
|
678
|
+
if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
|
|
679
|
+
const tool = lineToolsMap.get(selectedLineToolId);
|
|
680
|
+
const currentWidth = tool.options?.line?.width || 1;
|
|
681
|
+
const newWidth = Math.min(10, currentWidth + 1); // 최대 10
|
|
682
|
+
updateLineWidth(newWidth);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// 색상 드롭다운 토글
|
|
687
|
+
colorCurrentBtn.addEventListener('click', (e) => {
|
|
688
|
+
e.stopPropagation();
|
|
689
|
+
colorPalette.classList.toggle('show');
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// 색상 팔레트 외부 클릭시 닫기
|
|
693
|
+
document.addEventListener('click', (e) => {
|
|
694
|
+
if (!colorPalette.contains(e.target) && e.target !== colorCurrentBtn && e.target !== colorCurrentDisplay) {
|
|
695
|
+
colorPalette.classList.remove('show');
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// 색상 옵션 버튼들
|
|
700
|
+
colorPalette.querySelectorAll('.color-option').forEach(btn => {
|
|
701
|
+
btn.addEventListener('click', (e) => {
|
|
702
|
+
e.stopPropagation();
|
|
703
|
+
const color = btn.dataset.color;
|
|
704
|
+
|
|
705
|
+
// 현재 색상 디스플레이 업데이트
|
|
706
|
+
colorCurrentDisplay.style.background = color;
|
|
707
|
+
|
|
708
|
+
// 팔레트 닫기
|
|
709
|
+
colorPalette.classList.remove('show');
|
|
710
|
+
|
|
711
|
+
if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
|
|
712
|
+
console.log('🎨 색상 변경:', color);
|
|
713
|
+
const tool = lineToolsMap.get(selectedLineToolId);
|
|
714
|
+
console.log(' 선택된 도구:', selectedLineToolId, tool.toolType);
|
|
715
|
+
let updatedOptions;
|
|
716
|
+
|
|
717
|
+
if (tool.toolType === 'Text') {
|
|
718
|
+
// 텍스트 도구는 font.color 변경
|
|
719
|
+
updatedOptions = {
|
|
720
|
+
...tool.options,
|
|
721
|
+
text: {
|
|
722
|
+
...tool.options.text,
|
|
723
|
+
font: {
|
|
724
|
+
...tool.options.text.font,
|
|
725
|
+
color: color,
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
} else {
|
|
730
|
+
// 다른 도구들은 line.color 변경
|
|
731
|
+
updatedOptions = {
|
|
732
|
+
...tool.options,
|
|
733
|
+
line: {
|
|
734
|
+
...tool.options.line,
|
|
735
|
+
color: color,
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 기존 도구들을 모두 저장
|
|
741
|
+
const allTools = Array.from(lineToolsMap.values());
|
|
742
|
+
console.log(' 재생성할 도구 개수:', allTools.length);
|
|
743
|
+
|
|
744
|
+
// 모든 Line Tools 제거
|
|
745
|
+
chart.removeAllLineTools();
|
|
746
|
+
lineToolsMap.clear();
|
|
747
|
+
|
|
748
|
+
// 업데이트된 도구 포함하여 모두 재생성
|
|
749
|
+
let lastNewId = null;
|
|
750
|
+
allTools.forEach(t => {
|
|
751
|
+
const opts = t.id === selectedLineToolId ? updatedOptions : t.options;
|
|
752
|
+
const result = chart.addLineTool(t.toolType, t.points, opts);
|
|
753
|
+
|
|
754
|
+
const newId = getToolIdFromResult(result);
|
|
755
|
+
if (newId) {
|
|
756
|
+
console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId, '타입:', t.toolType);
|
|
757
|
+
lineToolsMap.set(newId, {
|
|
758
|
+
id: newId,
|
|
759
|
+
toolType: t.toolType,
|
|
760
|
+
points: t.points,
|
|
761
|
+
options: opts
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// 업데이트된 도구의 새 ID 저장
|
|
765
|
+
if (t.id === selectedLineToolId) {
|
|
766
|
+
lastNewId = newId;
|
|
767
|
+
console.log(' ✅ 선택된 도구의 새 ID:', newId);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// 재생성 후 업데이트된 도구의 새 ID를 선택
|
|
773
|
+
selectedLineToolId = lastNewId;
|
|
774
|
+
console.log(' 최종 selectedLineToolId:', selectedLineToolId);
|
|
775
|
+
console.log(' 최종 lineToolsMap 크기:', lineToolsMap.size);
|
|
776
|
+
|
|
777
|
+
// 마지막 설정값에 저장 (새 도구에 적용)
|
|
778
|
+
lastToolOptions.lineColor = color;
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// 텍스트 수정
|
|
784
|
+
contextMenu.querySelector('[data-action="edit-text"]').addEventListener('click', (e) => {
|
|
785
|
+
e.stopPropagation();
|
|
786
|
+
if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
|
|
787
|
+
const tool = lineToolsMap.get(selectedLineToolId);
|
|
788
|
+
if (tool.toolType === 'Text') {
|
|
789
|
+
const currentText = tool.options.text?.value || '';
|
|
790
|
+
showTextModal(currentText).then((newText) => {
|
|
791
|
+
if (newText !== null && newText !== '') {
|
|
792
|
+
const updatedOptions = {
|
|
793
|
+
...tool.options,
|
|
794
|
+
text: {
|
|
795
|
+
...tool.options.text,
|
|
796
|
+
value: newText,
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// 기존 도구들을 모두 저장
|
|
801
|
+
const allTools = Array.from(lineToolsMap.values());
|
|
802
|
+
|
|
803
|
+
// 모든 Line Tools 제거
|
|
804
|
+
chart.removeAllLineTools();
|
|
805
|
+
lineToolsMap.clear();
|
|
806
|
+
|
|
807
|
+
// 업데이트된 도구 포함하여 모두 재생성
|
|
808
|
+
let lastNewId = null;
|
|
809
|
+
allTools.forEach(t => {
|
|
810
|
+
const opts = t.id === selectedLineToolId ? updatedOptions : t.options;
|
|
811
|
+
const result = chart.addLineTool(t.toolType, t.points, opts);
|
|
812
|
+
|
|
813
|
+
const newId = getToolIdFromResult(result);
|
|
814
|
+
if (newId) {
|
|
815
|
+
lineToolsMap.set(newId, {
|
|
816
|
+
id: newId,
|
|
817
|
+
toolType: t.toolType,
|
|
818
|
+
points: t.points,
|
|
819
|
+
options: opts
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// 업데이트된 도구의 새 ID 저장
|
|
823
|
+
if (t.id === selectedLineToolId) {
|
|
824
|
+
lastNewId = newId;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// 재생성 후 업데이트된 도구의 새 ID를 선택
|
|
830
|
+
selectedLineToolId = lastNewId;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
hideContextMenu();
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// 삭제
|
|
839
|
+
contextMenu.querySelector('[data-action="delete"]').addEventListener('click', (e) => {
|
|
840
|
+
e.stopPropagation();
|
|
841
|
+
console.log('🗑️ 삭제 버튼 클릭');
|
|
842
|
+
if (selectedLineToolId !== null && lineToolsMap.has(selectedLineToolId)) {
|
|
843
|
+
console.log(' 삭제할 도구 ID:', selectedLineToolId);
|
|
844
|
+
// 기존 도구들을 모두 저장
|
|
845
|
+
const allTools = Array.from(lineToolsMap.values());
|
|
846
|
+
console.log(' 전체 도구 개수:', allTools.length);
|
|
847
|
+
|
|
848
|
+
// 선택된 도구 제외
|
|
849
|
+
const remainingTools = allTools.filter(t => t.id !== selectedLineToolId);
|
|
850
|
+
console.log(' 남은 도구 개수:', remainingTools.length);
|
|
851
|
+
|
|
852
|
+
// 모든 Line Tools 제거
|
|
853
|
+
chart.removeAllLineTools();
|
|
854
|
+
lineToolsMap.clear();
|
|
855
|
+
|
|
856
|
+
// 남은 도구들 재생성
|
|
857
|
+
let firstNewId = null;
|
|
858
|
+
remainingTools.forEach((t, index) => {
|
|
859
|
+
const result = chart.addLineTool(t.toolType, t.points, t.options);
|
|
860
|
+
|
|
861
|
+
const newId = getToolIdFromResult(result);
|
|
862
|
+
if (newId) {
|
|
863
|
+
console.log(' 도구 재생성: 기존 ID', t.id, '→ 새 ID', newId, '타입:', t.toolType);
|
|
864
|
+
lineToolsMap.set(newId, {
|
|
865
|
+
id: newId,
|
|
866
|
+
toolType: t.toolType,
|
|
867
|
+
points: t.points,
|
|
868
|
+
options: t.options
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// 첫 번째 재생성된 도구의 ID 저장
|
|
872
|
+
if (index === 0) {
|
|
873
|
+
firstNewId = newId;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
console.log(' 삭제 완료. 최종 lineToolsMap 크기:', lineToolsMap.size);
|
|
879
|
+
|
|
880
|
+
// 남은 도구가 있다면 첫 번째 도구를 자동으로 선택
|
|
881
|
+
if (firstNewId !== null) {
|
|
882
|
+
selectedLineToolId = firstNewId;
|
|
883
|
+
console.log(' ✅ 자동 선택된 도구 ID:', selectedLineToolId);
|
|
884
|
+
} else {
|
|
885
|
+
selectedLineToolId = null;
|
|
886
|
+
console.log(' ℹ️ 남은 도구가 없음');
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
hideContextMenu();
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// ===== Dropdown Menu Functionality =====
|
|
893
|
+
const indicatorBtn = document.getElementById('indicatorBtn');
|
|
894
|
+
const indicatorMenu = document.getElementById('indicatorMenu');
|
|
895
|
+
const drawingBtn = document.getElementById('drawingBtn');
|
|
896
|
+
const drawingMenu = document.getElementById('drawingMenu');
|
|
897
|
+
|
|
898
|
+
// Toggle indicator dropdown
|
|
899
|
+
indicatorBtn.addEventListener('click', (e) => {
|
|
900
|
+
e.stopPropagation();
|
|
901
|
+
indicatorMenu.classList.toggle('show');
|
|
902
|
+
drawingMenu.classList.remove('show');
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// Toggle drawing dropdown
|
|
906
|
+
drawingBtn.addEventListener('click', (e) => {
|
|
907
|
+
e.stopPropagation();
|
|
908
|
+
drawingMenu.classList.toggle('show');
|
|
909
|
+
indicatorMenu.classList.remove('show');
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Close dropdowns and color palettes when clicking outside
|
|
913
|
+
document.addEventListener('click', () => {
|
|
914
|
+
indicatorMenu.classList.remove('show');
|
|
915
|
+
drawingMenu.classList.remove('show');
|
|
916
|
+
// 색상 팔레트 닫기
|
|
917
|
+
document.querySelectorAll('.color-palette-popup').forEach(p => p.remove());
|
|
918
|
+
document.querySelectorAll('.period-color-picker').forEach(b => b.classList.remove('palette-active'));
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// Prevent dropdown from closing when clicking inside menu (except for menu items)
|
|
922
|
+
indicatorMenu.addEventListener('click', (e) => {
|
|
923
|
+
if (!e.target.classList.contains('dropdown-item')) {
|
|
924
|
+
e.stopPropagation();
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
drawingMenu.addEventListener('click', (e) => {
|
|
928
|
+
if (!e.target.classList.contains('dropdown-item')) {
|
|
929
|
+
e.stopPropagation();
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// Connect dropdown items to existing button handlers
|
|
934
|
+
// Drawing tools
|
|
935
|
+
document.querySelectorAll('#drawingMenu .dropdown-item').forEach(item => {
|
|
936
|
+
item.addEventListener('click', () => {
|
|
937
|
+
const btnId = item.id;
|
|
938
|
+
const originalBtn = document.getElementById(btnId);
|
|
939
|
+
if (originalBtn) {
|
|
940
|
+
originalBtn.click();
|
|
941
|
+
}
|
|
942
|
+
// Close dropdown after selection
|
|
943
|
+
drawingMenu.classList.remove('show');
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// ===== Indicator Settings in Dropdown =====
|
|
948
|
+
const indicatorSettingsSide = document.getElementById('indicatorSettingsSide');
|
|
949
|
+
|
|
950
|
+
// 지표별 설정 저장
|
|
951
|
+
export const indicatorConfigs = {
|
|
952
|
+
sma: [],
|
|
953
|
+
ema: [],
|
|
954
|
+
rsi: [],
|
|
955
|
+
macd: [],
|
|
956
|
+
bbands: []
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
// 지표별 기본 정보
|
|
960
|
+
export const indicatorInfo = {
|
|
961
|
+
sma: { title: '이동평균선', desc: '지난 n일 동안 주가 평균값을 이은 선', defaultValue: 20 },
|
|
962
|
+
ema: { title: '지수이동평균', desc: '최근 가격에 더 큰 가중치를 둔 이동평균선', defaultValue: 12 },
|
|
963
|
+
rsi: { title: 'RSI', desc: '상대강도지수 - 과매수/과매도 판단', defaultValue: 14 },
|
|
964
|
+
macd: { title: 'MACD', desc: '이동평균 수렴확산 - 추세 전환 신호', defaultValue: 26 },
|
|
965
|
+
bbands: { title: '볼린저 밴드', desc: '가격 변동성을 나타내는 밴드', defaultValue: 20 }
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// 색상 팔레트 (32색)
|
|
969
|
+
export const colorPaletteOptions = [
|
|
970
|
+
// Row 1: 어두운 톤
|
|
971
|
+
'#3C4043', '#1A73E8', '#9334E6', '#B80000', '#E37400', '#F9AB00', '#007B83', '#1E8E3E',
|
|
972
|
+
// Row 2: 기본 톤
|
|
973
|
+
'#5F6368', '#4285F4', '#A142F4', '#D93025', '#F57C00', '#F9AB00', '#12B5CB', '#34A853',
|
|
974
|
+
// Row 3: 밝은 톤
|
|
975
|
+
'#9AA0A6', '#8AB4F8', '#C58AF9', '#EE675C', '#FF8A65', '#FFB300', '#4DB6AC', '#81C995',
|
|
976
|
+
// Row 4: 연한 톤
|
|
977
|
+
'#DADCE0', '#AECBFA', '#D7AEFB', '#F28B82', '#FFAB91', '#FFD54F', '#80DEEA', '#A5D6A7'
|
|
978
|
+
];
|
|
979
|
+
|
|
980
|
+
// 기본 색상 (기존 호환용)
|
|
981
|
+
export const colorOptions = ['#26a69a', '#ef5350', '#2196f3', '#ff6f00', '#ab47bc', '#66bb6a', '#ffa726', '#42a5f5'];
|
|
982
|
+
|
|
983
|
+
export let currentSelectedIndicator = null;
|
|
984
|
+
|
|
985
|
+
// 지표 아이템 클릭 이벤트
|
|
986
|
+
document.querySelectorAll('.indicator-item').forEach(item => {
|
|
987
|
+
item.addEventListener('click', (e) => {
|
|
988
|
+
// 체크박스 클릭이면 토글
|
|
989
|
+
if (e.target.classList.contains('indicator-checkbox')) {
|
|
990
|
+
item.classList.toggle('checked');
|
|
991
|
+
const indicator = item.dataset.indicator;
|
|
992
|
+
|
|
993
|
+
if (!item.classList.contains('checked')) {
|
|
994
|
+
// 체크 해제 - 지표 삭제
|
|
995
|
+
indicatorConfigs[indicator] = [];
|
|
996
|
+
applyAllIndicators(); // 즉시 차트에서 제거
|
|
997
|
+
} else {
|
|
998
|
+
// 체크 - 기본 설정 추가
|
|
999
|
+
if (indicatorConfigs[indicator].length === 0) {
|
|
1000
|
+
// 이동평균선과 지수이동평균은 4개 기간 기본값
|
|
1001
|
+
if (indicator === 'sma' || indicator === 'ema') {
|
|
1002
|
+
const periods = [5, 20, 60, 120];
|
|
1003
|
+
periods.forEach((period, idx) => {
|
|
1004
|
+
indicatorConfigs[indicator].push({
|
|
1005
|
+
color: colorOptions[idx % colorOptions.length],
|
|
1006
|
+
thickness: 1,
|
|
1007
|
+
source: 'close',
|
|
1008
|
+
value: period
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
} else if (indicator === 'bbands') {
|
|
1012
|
+
// 볼린저 밴드는 각 라인 색상 설정 포함
|
|
1013
|
+
indicatorConfigs[indicator].push({
|
|
1014
|
+
color: colorOptions[0],
|
|
1015
|
+
thickness: 2,
|
|
1016
|
+
source: 'close',
|
|
1017
|
+
value: indicatorInfo[indicator].defaultValue,
|
|
1018
|
+
upperColor: '#F23645',
|
|
1019
|
+
middleColor: '#2962FF',
|
|
1020
|
+
lowerColor: '#089981',
|
|
1021
|
+
stdDev: 2
|
|
1022
|
+
});
|
|
1023
|
+
} else {
|
|
1024
|
+
// 다른 지표는 기본값 하나만
|
|
1025
|
+
const thickness = (indicator === 'rsi' || indicator === 'macd') ? 2 : 1;
|
|
1026
|
+
indicatorConfigs[indicator].push({
|
|
1027
|
+
color: colorOptions[0],
|
|
1028
|
+
thickness: thickness,
|
|
1029
|
+
source: 'close',
|
|
1030
|
+
value: indicatorInfo[indicator].defaultValue
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
applyAllIndicators(); // 즉시 차트에 추가
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// 선택 상태 업데이트
|
|
1039
|
+
document.querySelectorAll('.indicator-item').forEach(i => i.classList.remove('selected'));
|
|
1040
|
+
item.classList.add('selected');
|
|
1041
|
+
currentSelectedIndicator = item.dataset.indicator;
|
|
1042
|
+
|
|
1043
|
+
// 오른쪽 설정 화면 렌더링
|
|
1044
|
+
renderIndicatorSettings(currentSelectedIndicator);
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
function renderIndicatorSettings(indicator) {
|
|
1049
|
+
if (!indicator) {
|
|
1050
|
+
indicatorSettingsSide.innerHTML = '<div class="indicator-empty-state">지표를 선택하세요</div>';
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const info = indicatorInfo[indicator];
|
|
1055
|
+
const configs = indicatorConfigs[indicator];
|
|
1056
|
+
|
|
1057
|
+
let html = `
|
|
1058
|
+
<div class="indicator-settings-title">${info.title}</div>
|
|
1059
|
+
<div class="indicator-settings-desc">${info.desc}</div>
|
|
1060
|
+
`;
|
|
1061
|
+
|
|
1062
|
+
if (configs.length === 0) {
|
|
1063
|
+
html += '<div class="indicator-empty-state">체크박스를 클릭하여 지표를 활성화하세요</div>';
|
|
1064
|
+
} else {
|
|
1065
|
+
// 하단 지표 (RSI, MACD)는 간단한 UI
|
|
1066
|
+
if (indicator === 'rsi') {
|
|
1067
|
+
const config = configs[0];
|
|
1068
|
+
html += `
|
|
1069
|
+
<div style="margin-top: 16px;">
|
|
1070
|
+
<div style="display: flex; gap: 12px; align-items: flex-start;">
|
|
1071
|
+
<div style="flex: 1;">
|
|
1072
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">색상</div>
|
|
1073
|
+
<div class="period-color-picker rsi-color-picker" data-indicator="${indicator}" data-index="0" style="background: ${config.color}; width: 100%; height: 40px;"></div>
|
|
1074
|
+
</div>
|
|
1075
|
+
<div style="flex: 1;">
|
|
1076
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">기간</div>
|
|
1077
|
+
<input type="number" class="period-value-field" data-indicator="${indicator}" data-index="0" value="${config.value}" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
|
|
1078
|
+
</div>
|
|
1079
|
+
</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
`;
|
|
1082
|
+
} else if (indicator === 'macd') {
|
|
1083
|
+
const config = configs[0];
|
|
1084
|
+
html += `
|
|
1085
|
+
<div style="margin-top: 16px;">
|
|
1086
|
+
<!-- 색상 설정 (한 줄) -->
|
|
1087
|
+
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
|
|
1088
|
+
<div style="flex: 1;">
|
|
1089
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">MACD</div>
|
|
1090
|
+
<div class="period-color-picker macd-line-color-picker" data-macd-type="line" style="background: #2962FF; width: 100%; height: 40px;"></div>
|
|
1091
|
+
</div>
|
|
1092
|
+
<div style="flex: 1;">
|
|
1093
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">Signal</div>
|
|
1094
|
+
<div class="period-color-picker macd-signal-color-picker" data-macd-type="signal" style="background: #FF6D00; width: 100%; height: 40px;"></div>
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
|
|
1098
|
+
<!-- 파라미터 설정 (한 줄) -->
|
|
1099
|
+
<div style="display: flex; gap: 8px;">
|
|
1100
|
+
<div style="flex: 1;">
|
|
1101
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">단기</div>
|
|
1102
|
+
<input type="number" class="macd-fast-field" value="12" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
|
|
1103
|
+
</div>
|
|
1104
|
+
<div style="flex: 1;">
|
|
1105
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">장기</div>
|
|
1106
|
+
<input type="number" class="macd-slow-field" value="26" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
|
|
1107
|
+
</div>
|
|
1108
|
+
<div style="flex: 1;">
|
|
1109
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">시그널</div>
|
|
1110
|
+
<input type="number" class="macd-signal-field" value="9" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
|
|
1111
|
+
</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
`;
|
|
1115
|
+
} else if (indicator === 'bbands') {
|
|
1116
|
+
const config = configs[0];
|
|
1117
|
+
html += `
|
|
1118
|
+
<div style="margin-top: 16px;">
|
|
1119
|
+
<!-- 색상 설정 (한 줄) -->
|
|
1120
|
+
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
|
|
1121
|
+
<div style="flex: 1;">
|
|
1122
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">상단</div>
|
|
1123
|
+
<div class="period-color-picker bbands-upper-color-picker" data-bbands-type="upper" style="background: ${config.upperColor || '#F23645'}; width: 100%; height: 40px;"></div>
|
|
1124
|
+
</div>
|
|
1125
|
+
<div style="flex: 1;">
|
|
1126
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">중간</div>
|
|
1127
|
+
<div class="period-color-picker bbands-middle-color-picker" data-bbands-type="middle" style="background: ${config.middleColor || '#2962FF'}; width: 100%; height: 40px;"></div>
|
|
1128
|
+
</div>
|
|
1129
|
+
<div style="flex: 1;">
|
|
1130
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">하단</div>
|
|
1131
|
+
<div class="period-color-picker bbands-lower-color-picker" data-bbands-type="lower" style="background: ${config.lowerColor || '#089981'}; width: 100%; height: 40px;"></div>
|
|
1132
|
+
</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
|
|
1135
|
+
<!-- 파라미터 설정 (한 줄) -->
|
|
1136
|
+
<div style="display: flex; gap: 8px;">
|
|
1137
|
+
<div style="flex: 1;">
|
|
1138
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">기간</div>
|
|
1139
|
+
<input type="number" class="bbands-period-field" value="${config.value || 20}" min="1" max="500" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
|
|
1140
|
+
</div>
|
|
1141
|
+
<div style="flex: 1;">
|
|
1142
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">표준편차</div>
|
|
1143
|
+
<input type="number" class="bbands-stddev-field" value="${config.stdDev || 2}" min="0.5" max="5" step="0.1" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; height: 40px; box-sizing: border-box;">
|
|
1144
|
+
</div>
|
|
1145
|
+
<div style="flex: 1;">
|
|
1146
|
+
<div style="font-size: 13px; color: #666; margin-bottom: 8px;">두께</div>
|
|
1147
|
+
<div class="bbands-thickness-display" style="width: 100%; height: 40px; border: 1px solid #e0e0e0; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 500; cursor: pointer; user-select: none; background: white;">${config.thickness || 2}px</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
</div>
|
|
1150
|
+
</div>
|
|
1151
|
+
`;
|
|
1152
|
+
} else {
|
|
1153
|
+
// 상단 지표 (SMA, EMA, BBands)는 기존 UI
|
|
1154
|
+
configs.forEach((config, index) => {
|
|
1155
|
+
html += `
|
|
1156
|
+
<div class="indicator-period-row">
|
|
1157
|
+
<span class="period-label-col">기간${index + 1}</span>
|
|
1158
|
+
<div class="period-color-picker" data-indicator="${indicator}" data-index="${index}" style="background: ${config.color};"></div>
|
|
1159
|
+
<div class="period-thickness-display" data-indicator="${indicator}" data-index="${index}">${config.thickness}px</div>
|
|
1160
|
+
<select class="period-source-dropdown" data-indicator="${indicator}" data-index="${index}">
|
|
1161
|
+
<option value="close" ${config.source === 'close' ? 'selected' : ''}>종가</option>
|
|
1162
|
+
<option value="open" ${config.source === 'open' ? 'selected' : ''}>시가</option>
|
|
1163
|
+
<option value="high" ${config.source === 'high' ? 'selected' : ''}>고가</option>
|
|
1164
|
+
<option value="low" ${config.source === 'low' ? 'selected' : ''}>저가</option>
|
|
1165
|
+
</select>
|
|
1166
|
+
<input type="number" class="period-value-field" data-indicator="${indicator}" data-index="${index}" value="${config.value}" min="1" max="500">
|
|
1167
|
+
${configs.length > 1 ? `<button class="period-delete-btn" data-indicator="${indicator}" data-index="${index}">×</button>` : ''}
|
|
1168
|
+
</div>
|
|
1169
|
+
`;
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
html += `
|
|
1173
|
+
<button class="add-period-button" data-indicator="${indicator}">
|
|
1174
|
+
+ 기간 추가
|
|
1175
|
+
</button>
|
|
1176
|
+
`;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
indicatorSettingsSide.innerHTML = html;
|
|
1181
|
+
attachIndicatorSettingsEvents();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function attachIndicatorSettingsEvents() {
|
|
1185
|
+
// 색상 변경 - 팔레트 표시
|
|
1186
|
+
document.querySelectorAll('.period-color-picker').forEach(btn => {
|
|
1187
|
+
btn.addEventListener('click', (e) => {
|
|
1188
|
+
e.stopPropagation();
|
|
1189
|
+
const indicator = btn.dataset.indicator;
|
|
1190
|
+
const index = parseInt(btn.dataset.index);
|
|
1191
|
+
|
|
1192
|
+
// 현재 색상 가져오기
|
|
1193
|
+
let currentColor;
|
|
1194
|
+
if (btn.classList.contains('macd-line-color-picker') || btn.classList.contains('macd-signal-color-picker')) {
|
|
1195
|
+
currentColor = btn.style.background;
|
|
1196
|
+
} else if (indicator && index !== undefined) {
|
|
1197
|
+
currentColor = indicatorConfigs[indicator][index].color;
|
|
1198
|
+
} else {
|
|
1199
|
+
currentColor = '#26a69a';
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// 이미 이 버튼이 팔레트를 열어놓았는지 확인
|
|
1203
|
+
const existingPalette = document.querySelector('.color-palette-popup');
|
|
1204
|
+
if (existingPalette && btn.classList.contains('palette-active')) {
|
|
1205
|
+
// 같은 버튼 다시 클릭 시 팔레트 닫기
|
|
1206
|
+
existingPalette.remove();
|
|
1207
|
+
btn.classList.remove('palette-active');
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// 기존 팔레트 제거 및 모든 버튼의 active 상태 제거
|
|
1212
|
+
document.querySelectorAll('.color-palette-popup').forEach(p => p.remove());
|
|
1213
|
+
document.querySelectorAll('.period-color-picker').forEach(b => b.classList.remove('palette-active'));
|
|
1214
|
+
|
|
1215
|
+
// 현재 버튼을 active로 표시
|
|
1216
|
+
btn.classList.add('palette-active');
|
|
1217
|
+
|
|
1218
|
+
// 버튼 위치 계산
|
|
1219
|
+
const rect = btn.getBoundingClientRect();
|
|
1220
|
+
|
|
1221
|
+
// 팔레트 HTML 생성 (버튼 바로 아래)
|
|
1222
|
+
let paletteHTML = `
|
|
1223
|
+
<div class="color-palette-popup show" style="top: ${rect.bottom + 5}px; left: ${rect.left}px;">
|
|
1224
|
+
<div class="color-palette-title">컬러</div>
|
|
1225
|
+
<div class="color-palette-grid">
|
|
1226
|
+
`;
|
|
1227
|
+
|
|
1228
|
+
colorPaletteOptions.forEach(color => {
|
|
1229
|
+
const selected = color === currentColor ? 'selected' : '';
|
|
1230
|
+
paletteHTML += `<div class="color-palette-item ${selected}" style="background: ${color};" data-color="${color}"></div>`;
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
paletteHTML += `
|
|
1234
|
+
</div>
|
|
1235
|
+
</div>
|
|
1236
|
+
`;
|
|
1237
|
+
|
|
1238
|
+
// body에 팔레트 추가
|
|
1239
|
+
document.body.insertAdjacentHTML('beforeend', paletteHTML);
|
|
1240
|
+
|
|
1241
|
+
// 팔레트 아이템 클릭 이벤트
|
|
1242
|
+
const palette = document.querySelector('.color-palette-popup');
|
|
1243
|
+
palette.querySelectorAll('.color-palette-item').forEach(item => {
|
|
1244
|
+
item.addEventListener('click', (e) => {
|
|
1245
|
+
e.stopPropagation();
|
|
1246
|
+
const selectedColor = item.dataset.color;
|
|
1247
|
+
|
|
1248
|
+
// MACD 색상 picker인 경우
|
|
1249
|
+
if (btn.classList.contains('macd-line-color-picker') || btn.classList.contains('macd-signal-color-picker')) {
|
|
1250
|
+
btn.style.background = selectedColor;
|
|
1251
|
+
palette.remove();
|
|
1252
|
+
btn.classList.remove('palette-active');
|
|
1253
|
+
applyAllIndicators();
|
|
1254
|
+
} else if (btn.classList.contains('bbands-upper-color-picker') ||
|
|
1255
|
+
btn.classList.contains('bbands-middle-color-picker') ||
|
|
1256
|
+
btn.classList.contains('bbands-lower-color-picker')) {
|
|
1257
|
+
// 볼린저 밴드 색상 picker인 경우
|
|
1258
|
+
btn.style.background = selectedColor;
|
|
1259
|
+
const bbandsType = btn.dataset.bbandsType;
|
|
1260
|
+
const config = indicatorConfigs['bbands'][0];
|
|
1261
|
+
if (bbandsType === 'upper') {
|
|
1262
|
+
config.upperColor = selectedColor;
|
|
1263
|
+
} else if (bbandsType === 'middle') {
|
|
1264
|
+
config.middleColor = selectedColor;
|
|
1265
|
+
} else if (bbandsType === 'lower') {
|
|
1266
|
+
config.lowerColor = selectedColor;
|
|
1267
|
+
}
|
|
1268
|
+
palette.remove();
|
|
1269
|
+
btn.classList.remove('palette-active');
|
|
1270
|
+
applyAllIndicators();
|
|
1271
|
+
} else if (indicator && index !== undefined) {
|
|
1272
|
+
// 일반 지표인 경우
|
|
1273
|
+
indicatorConfigs[indicator][index].color = selectedColor;
|
|
1274
|
+
palette.remove();
|
|
1275
|
+
btn.classList.remove('palette-active');
|
|
1276
|
+
renderIndicatorSettings(indicator);
|
|
1277
|
+
applyAllIndicators();
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
// 두께 변경
|
|
1285
|
+
document.querySelectorAll('.period-thickness-display').forEach(btn => {
|
|
1286
|
+
btn.addEventListener('click', () => {
|
|
1287
|
+
const indicator = btn.dataset.indicator;
|
|
1288
|
+
const index = parseInt(btn.dataset.index);
|
|
1289
|
+
indicatorConfigs[indicator][index].thickness = (indicatorConfigs[indicator][index].thickness % 5) + 1;
|
|
1290
|
+
renderIndicatorSettings(indicator);
|
|
1291
|
+
applyAllIndicators(); // 즉시 차트에 반영
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
// 소스 변경
|
|
1296
|
+
document.querySelectorAll('.period-source-dropdown').forEach(select => {
|
|
1297
|
+
select.addEventListener('change', (e) => {
|
|
1298
|
+
const indicator = select.dataset.indicator;
|
|
1299
|
+
const index = parseInt(select.dataset.index);
|
|
1300
|
+
indicatorConfigs[indicator][index].source = e.target.value;
|
|
1301
|
+
applyAllIndicators(); // 즉시 차트에 반영
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// 값 변경
|
|
1306
|
+
document.querySelectorAll('.period-value-field').forEach(input => {
|
|
1307
|
+
input.addEventListener('change', (e) => {
|
|
1308
|
+
const indicator = input.dataset.indicator;
|
|
1309
|
+
const index = parseInt(input.dataset.index);
|
|
1310
|
+
indicatorConfigs[indicator][index].value = parseInt(e.target.value) || 1;
|
|
1311
|
+
applyAllIndicators(); // 즉시 차트에 반영
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
// 삭제
|
|
1316
|
+
document.querySelectorAll('.period-delete-btn').forEach(btn => {
|
|
1317
|
+
btn.addEventListener('click', () => {
|
|
1318
|
+
const indicator = btn.dataset.indicator;
|
|
1319
|
+
const index = parseInt(btn.dataset.index);
|
|
1320
|
+
indicatorConfigs[indicator].splice(index, 1);
|
|
1321
|
+
renderIndicatorSettings(indicator);
|
|
1322
|
+
applyAllIndicators(); // 즉시 차트에 반영
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
// 기간 추가
|
|
1327
|
+
document.querySelectorAll('.add-period-button').forEach(btn => {
|
|
1328
|
+
btn.addEventListener('click', () => {
|
|
1329
|
+
const indicator = btn.dataset.indicator;
|
|
1330
|
+
const lastConfig = indicatorConfigs[indicator][indicatorConfigs[indicator].length - 1];
|
|
1331
|
+
const nextColorIdx = (colorOptions.indexOf(lastConfig.color) + 1) % colorOptions.length;
|
|
1332
|
+
indicatorConfigs[indicator].push({
|
|
1333
|
+
color: colorOptions[nextColorIdx],
|
|
1334
|
+
thickness: 1,
|
|
1335
|
+
source: 'close',
|
|
1336
|
+
value: indicatorInfo[indicator].defaultValue
|
|
1337
|
+
});
|
|
1338
|
+
renderIndicatorSettings(indicator);
|
|
1339
|
+
applyAllIndicators(); // 즉시 차트에 반영
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// MACD 파라미터 변경
|
|
1344
|
+
const macdFast = document.querySelector('.macd-fast-field');
|
|
1345
|
+
const macdSlow = document.querySelector('.macd-slow-field');
|
|
1346
|
+
const macdSignal = document.querySelector('.macd-signal-field');
|
|
1347
|
+
|
|
1348
|
+
if (macdFast) {
|
|
1349
|
+
macdFast.addEventListener('change', () => {
|
|
1350
|
+
applyAllIndicators();
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
if (macdSlow) {
|
|
1354
|
+
macdSlow.addEventListener('change', () => {
|
|
1355
|
+
applyAllIndicators();
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
if (macdSignal) {
|
|
1359
|
+
macdSignal.addEventListener('change', () => {
|
|
1360
|
+
applyAllIndicators();
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Bollinger Bands 파라미터 변경
|
|
1365
|
+
const bbandsPeriod = document.querySelector('.bbands-period-field');
|
|
1366
|
+
const bbandsStdDev = document.querySelector('.bbands-stddev-field');
|
|
1367
|
+
const bbandsThickness = document.querySelector('.bbands-thickness-display');
|
|
1368
|
+
|
|
1369
|
+
if (bbandsPeriod) {
|
|
1370
|
+
bbandsPeriod.addEventListener('change', (e) => {
|
|
1371
|
+
const config = indicatorConfigs['bbands'][0];
|
|
1372
|
+
config.value = parseInt(e.target.value) || 20;
|
|
1373
|
+
applyAllIndicators();
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
if (bbandsStdDev) {
|
|
1377
|
+
bbandsStdDev.addEventListener('change', (e) => {
|
|
1378
|
+
const config = indicatorConfigs['bbands'][0];
|
|
1379
|
+
config.stdDev = parseFloat(e.target.value) || 2;
|
|
1380
|
+
applyAllIndicators();
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
if (bbandsThickness) {
|
|
1384
|
+
bbandsThickness.addEventListener('click', () => {
|
|
1385
|
+
const config = indicatorConfigs['bbands'][0];
|
|
1386
|
+
config.thickness = (config.thickness % 5) + 1;
|
|
1387
|
+
renderIndicatorSettings('bbands');
|
|
1388
|
+
applyAllIndicators();
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// 드롭다운이 닫힐 때 체크박스 상태 동기화
|
|
1394
|
+
document.addEventListener('click', (e) => {
|
|
1395
|
+
if (!indicatorMenu.contains(e.target) && !indicatorBtn.contains(e.target)) {
|
|
1396
|
+
if (indicatorMenu.classList.contains('show')) {
|
|
1397
|
+
// 체크박스 상태 기반으로 indicatorConfigs 재구성
|
|
1398
|
+
document.querySelectorAll('.indicator-item').forEach(item => {
|
|
1399
|
+
const indicator = item.dataset.indicator;
|
|
1400
|
+
if (item.classList.contains('checked')) {
|
|
1401
|
+
// 체크되어 있는데 설정이 비어있으면 기본값 추가
|
|
1402
|
+
if (indicatorConfigs[indicator].length === 0) {
|
|
1403
|
+
// 이동평균선과 지수이동평균은 4개 기간 기본값
|
|
1404
|
+
if (indicator === 'sma' || indicator === 'ema') {
|
|
1405
|
+
const periods = [5, 20, 60, 120];
|
|
1406
|
+
periods.forEach((period, idx) => {
|
|
1407
|
+
indicatorConfigs[indicator].push({
|
|
1408
|
+
color: colorOptions[idx % colorOptions.length],
|
|
1409
|
+
thickness: 1,
|
|
1410
|
+
source: 'close',
|
|
1411
|
+
value: period
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
} else if (indicator === 'bbands') {
|
|
1415
|
+
// 볼린저 밴드는 각 라인 색상 설정 포함
|
|
1416
|
+
indicatorConfigs[indicator].push({
|
|
1417
|
+
color: colorOptions[0],
|
|
1418
|
+
thickness: 2,
|
|
1419
|
+
source: 'close',
|
|
1420
|
+
value: indicatorInfo[indicator].defaultValue,
|
|
1421
|
+
upperColor: '#F23645',
|
|
1422
|
+
middleColor: '#2962FF',
|
|
1423
|
+
lowerColor: '#089981',
|
|
1424
|
+
stdDev: 2
|
|
1425
|
+
});
|
|
1426
|
+
} else {
|
|
1427
|
+
// 다른 지표는 기본값 하나만
|
|
1428
|
+
const thickness = (indicator === 'rsi' || indicator === 'macd') ? 2 : 1;
|
|
1429
|
+
indicatorConfigs[indicator].push({
|
|
1430
|
+
color: colorOptions[0],
|
|
1431
|
+
thickness: thickness,
|
|
1432
|
+
source: 'close',
|
|
1433
|
+
value: indicatorInfo[indicator].defaultValue
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
} else {
|
|
1438
|
+
// 체크 해제되어 있으면 설정 비우기
|
|
1439
|
+
indicatorConfigs[indicator] = [];
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
indicatorMenu.classList.remove('show');
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
function applyAllIndicators() {
|
|
1449
|
+
// 기존 동적 지표 제거
|
|
1450
|
+
window.dynamicIndicatorSeries.forEach(series => {
|
|
1451
|
+
window.chart.removeSeries(series);
|
|
1452
|
+
});
|
|
1453
|
+
window.dynamicIndicatorSeries.length = 0; // 배열 비우기
|
|
1454
|
+
|
|
1455
|
+
Object.keys(indicatorConfigs).forEach(type => {
|
|
1456
|
+
indicatorConfigs[type].forEach(config => {
|
|
1457
|
+
let data;
|
|
1458
|
+
|
|
1459
|
+
if (type === 'sma') {
|
|
1460
|
+
data = calculateSMA(window.currentCandles, { period: config.value });
|
|
1461
|
+
if (data && data.length > 0) {
|
|
1462
|
+
const series = window.chart.addLineSeries({
|
|
1463
|
+
color: config.color,
|
|
1464
|
+
lineWidth: config.thickness,
|
|
1465
|
+
title: `SMA ${config.value}`,
|
|
1466
|
+
});
|
|
1467
|
+
series.setData(data);
|
|
1468
|
+
window.dynamicIndicatorSeries.push(series);
|
|
1469
|
+
}
|
|
1470
|
+
} else if (type === 'ema') {
|
|
1471
|
+
data = calculateEMA(window.currentCandles, { period: config.value });
|
|
1472
|
+
if (data && data.length > 0) {
|
|
1473
|
+
const series = window.chart.addLineSeries({
|
|
1474
|
+
color: config.color,
|
|
1475
|
+
lineWidth: config.thickness,
|
|
1476
|
+
title: `EMA ${config.value}`,
|
|
1477
|
+
});
|
|
1478
|
+
series.setData(data);
|
|
1479
|
+
window.dynamicIndicatorSeries.push(series);
|
|
1480
|
+
}
|
|
1481
|
+
} else if (type === 'rsi') {
|
|
1482
|
+
data = calculateRSI(window.currentCandles, { period: config.value });
|
|
1483
|
+
if (data && data.length > 0) {
|
|
1484
|
+
const series = window.chart.addLineSeries({
|
|
1485
|
+
color: config.color,
|
|
1486
|
+
lineWidth: config.thickness,
|
|
1487
|
+
title: `RSI ${config.value}`,
|
|
1488
|
+
priceScaleId: 'rsi',
|
|
1489
|
+
});
|
|
1490
|
+
series.priceScale().applyOptions({
|
|
1491
|
+
scaleMargins: { top: 0.8, bottom: 0 },
|
|
1492
|
+
});
|
|
1493
|
+
series.setData(data);
|
|
1494
|
+
window.dynamicIndicatorSeries.push(series);
|
|
1495
|
+
}
|
|
1496
|
+
} else if (type === 'macd') {
|
|
1497
|
+
// MACD 파라미터와 색상을 UI에서 읽기
|
|
1498
|
+
const fastField = document.querySelector('.macd-fast-field');
|
|
1499
|
+
const slowField = document.querySelector('.macd-slow-field');
|
|
1500
|
+
const signalField = document.querySelector('.macd-signal-field');
|
|
1501
|
+
const macdColorPicker = document.querySelector('.macd-line-color-picker');
|
|
1502
|
+
const signalColorPicker = document.querySelector('.macd-signal-color-picker');
|
|
1503
|
+
|
|
1504
|
+
const fastPeriod = fastField ? parseInt(fastField.value) || 12 : 12;
|
|
1505
|
+
const slowPeriod = slowField ? parseInt(slowField.value) || 26 : 26;
|
|
1506
|
+
const signalPeriod = signalField ? parseInt(signalField.value) || 9 : 9;
|
|
1507
|
+
const macdColor = macdColorPicker ? macdColorPicker.style.background : '#2962FF';
|
|
1508
|
+
const signalColor = signalColorPicker ? signalColorPicker.style.background : '#FF6D00';
|
|
1509
|
+
|
|
1510
|
+
const macdData = calculateMACD(window.currentCandles, { fastPeriod, slowPeriod, signalPeriod });
|
|
1511
|
+
if (macdData.macd && macdData.macd.length > 0) {
|
|
1512
|
+
const macdSeries = window.chart.addLineSeries({
|
|
1513
|
+
color: macdColor,
|
|
1514
|
+
lineWidth: config.thickness,
|
|
1515
|
+
title: 'MACD',
|
|
1516
|
+
priceScaleId: 'macd',
|
|
1517
|
+
});
|
|
1518
|
+
macdSeries.priceScale().applyOptions({
|
|
1519
|
+
scaleMargins: { top: 0.8, bottom: 0 },
|
|
1520
|
+
});
|
|
1521
|
+
macdSeries.setData(macdData.macd);
|
|
1522
|
+
window.dynamicIndicatorSeries.push(macdSeries);
|
|
1523
|
+
|
|
1524
|
+
// Signal line
|
|
1525
|
+
const signalSeries = window.chart.addLineSeries({
|
|
1526
|
+
color: signalColor,
|
|
1527
|
+
lineWidth: config.thickness,
|
|
1528
|
+
title: 'Signal',
|
|
1529
|
+
priceScaleId: 'macd',
|
|
1530
|
+
});
|
|
1531
|
+
signalSeries.setData(macdData.signal);
|
|
1532
|
+
window.dynamicIndicatorSeries.push(signalSeries);
|
|
1533
|
+
|
|
1534
|
+
// Histogram
|
|
1535
|
+
const histSeries = window.chart.addHistogramSeries({
|
|
1536
|
+
color: '#26a69a',
|
|
1537
|
+
priceScaleId: 'macd',
|
|
1538
|
+
});
|
|
1539
|
+
histSeries.setData(macdData.histogram);
|
|
1540
|
+
window.dynamicIndicatorSeries.push(histSeries);
|
|
1541
|
+
}
|
|
1542
|
+
} else if (type === 'bbands') {
|
|
1543
|
+
const stdDev = config.stdDev || 2;
|
|
1544
|
+
const bbData = calculateBollingerBands(window.currentCandles, { period: config.value, stdDev });
|
|
1545
|
+
if (bbData.upper && bbData.upper.length > 0) {
|
|
1546
|
+
// 볼린저 밴드는 각 라인마다 다른 색상 사용
|
|
1547
|
+
const upperColor = config.upperColor || '#F23645'; // 빨강
|
|
1548
|
+
const middleColor = config.middleColor || '#2962FF'; // 파랑
|
|
1549
|
+
const lowerColor = config.lowerColor || '#089981'; // 초록
|
|
1550
|
+
|
|
1551
|
+
const upperSeries = window.chart.addLineSeries({
|
|
1552
|
+
color: upperColor,
|
|
1553
|
+
lineWidth: config.thickness,
|
|
1554
|
+
title: 'BB Upper',
|
|
1555
|
+
});
|
|
1556
|
+
upperSeries.setData(bbData.upper);
|
|
1557
|
+
window.dynamicIndicatorSeries.push(upperSeries);
|
|
1558
|
+
|
|
1559
|
+
const middleSeries = window.chart.addLineSeries({
|
|
1560
|
+
color: middleColor,
|
|
1561
|
+
lineWidth: config.thickness,
|
|
1562
|
+
title: 'BB Middle',
|
|
1563
|
+
});
|
|
1564
|
+
middleSeries.setData(bbData.middle);
|
|
1565
|
+
window.dynamicIndicatorSeries.push(middleSeries);
|
|
1566
|
+
|
|
1567
|
+
const lowerSeries = window.chart.addLineSeries({
|
|
1568
|
+
color: lowerColor,
|
|
1569
|
+
lineWidth: config.thickness,
|
|
1570
|
+
title: 'BB Lower',
|
|
1571
|
+
});
|
|
1572
|
+
lowerSeries.setData(bbData.lower);
|
|
1573
|
+
window.dynamicIndicatorSeries.push(lowerSeries);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// ===== Custom Text Modal Functionality =====
|
|
1581
|
+
// ===== Custom Text Modal Functionality =====
|
|
1582
|
+
const textModal = document.getElementById('textModal');
|
|
1583
|
+
const textInput = document.getElementById('textInput');
|
|
1584
|
+
const textModalConfirm = document.getElementById('textModalConfirm');
|
|
1585
|
+
const textModalCancel = document.getElementById('textModalCancel');
|
|
1586
|
+
|
|
1587
|
+
let textModalResolve = null;
|
|
1588
|
+
|
|
1589
|
+
function showTextModal(defaultValue = '') {
|
|
1590
|
+
return new Promise((resolve) => {
|
|
1591
|
+
textModalResolve = resolve;
|
|
1592
|
+
textInput.value = defaultValue;
|
|
1593
|
+
textModal.style.display = 'flex';
|
|
1594
|
+
setTimeout(() => {
|
|
1595
|
+
textInput.focus();
|
|
1596
|
+
textInput.select();
|
|
1597
|
+
}, 100);
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function hideTextModal() {
|
|
1602
|
+
textModal.style.display = 'none';
|
|
1603
|
+
textInput.value = '';
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// Confirm button
|
|
1607
|
+
textModalConfirm.addEventListener('click', () => {
|
|
1608
|
+
const value = textInput.value;
|
|
1609
|
+
hideTextModal();
|
|
1610
|
+
if (textModalResolve) {
|
|
1611
|
+
textModalResolve(value);
|
|
1612
|
+
textModalResolve = null;
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
// Cancel button
|
|
1617
|
+
textModalCancel.addEventListener('click', () => {
|
|
1618
|
+
hideTextModal();
|
|
1619
|
+
if (textModalResolve) {
|
|
1620
|
+
textModalResolve(null);
|
|
1621
|
+
textModalResolve = null;
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// Enter key to confirm
|
|
1626
|
+
textInput.addEventListener('keydown', (e) => {
|
|
1627
|
+
if (e.key === 'Enter') {
|
|
1628
|
+
e.preventDefault();
|
|
1629
|
+
textModalConfirm.click();
|
|
1630
|
+
} else if (e.key === 'Escape') {
|
|
1631
|
+
e.preventDefault();
|
|
1632
|
+
textModalCancel.click();
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
// Close modal when clicking overlay
|
|
1637
|
+
textModal.addEventListener('click', (e) => {
|
|
1638
|
+
if (e.target === textModal) {
|
|
1639
|
+
textModalCancel.click();
|
|
1640
|
+
}
|
|
1641
|
+
});
|