Skip to content

Commit 71fb299

Browse files
committed
feat: 과거데이터 패치 함수 props 추가
issue: #33
1 parent ba33d0d commit 71fb299

File tree

6 files changed

+247
-83
lines changed

6 files changed

+247
-83
lines changed

src/features/tradeview/ui/Chart/MainPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function MainPanel({
2323
am5stock.StockPanel.new(chartRoot, {
2424
wheelY: 'zoomX',
2525
panX: true,
26-
panY: true,
26+
panY: false,
2727
}),
2828
);
2929

src/features/tradeview/ui/Chart/StockChart.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default function StockChart({
2727
return;
2828
}
2929

30+
chartRoot.container.children.clear();
3031
const stockChart = chartRoot?.container.children.push(
3132
am5stock.StockChart.new(chartRoot, settings),
3233
);
Lines changed: 70 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import * as am5stock from '@amcharts/amcharts5/stock';
22
import * as am5xy from '@amcharts/amcharts5/xy';
3-
import { type PropsWithChildren, useEffect, useState } from 'react';
3+
import type { PropsWithChildren } from 'react';
44

55
import { isNullish } from '~/shared/utils';
66
import type { CandlestickData } from '../../types/tradeview.type';
77
import { isDisposed } from '../../utils';
88
import type { StockAxis } from './StockAxis';
99

10-
type SeriesSettings = {
10+
export type SeriesSettings = {
1111
name?: string;
1212
clustered?: boolean;
1313
legendValueText?: string;
1414
legendRangeValueText?: string;
1515
};
1616

17-
type ValueSeriesProps = PropsWithChildren<
17+
export type ValueSeriesProps = PropsWithChildren<
1818
Partial<StockAxis> & {
1919
pastTimeData?: CandlestickData[];
2020
seriesSettings?: SeriesSettings;
21+
fetchPastTimeData?: () => Promise<void>;
2122
}
2223
>;
2324

@@ -30,86 +31,82 @@ export default function ValueSeries({
3031
dateAxis,
3132
pastTimeData,
3233
seriesSettings,
34+
fetchPastTimeData,
3335
}: ValueSeriesProps) {
34-
const [valueSeries, setValueSeries] =
35-
useState<am5xy.CandlestickSeries | null>(null);
36+
if (
37+
isNullish(chartRoot) ||
38+
isNullish(stockChart) ||
39+
isNullish(mainPanel) ||
40+
isNullish(valueAxis) ||
41+
isNullish(dateAxis)
42+
) {
43+
console.error('ValueSeries should be used within StockAxis');
44+
return;
45+
}
3646

37-
useEffect(() => {
38-
if (!valueSeries || !pastTimeData) return;
39-
valueSeries.data.setAll(pastTimeData);
40-
}, [valueSeries, pastTimeData]);
47+
if (isDisposed(chartRoot, stockChart, mainPanel, valueAxis, dateAxis)) return;
4148

42-
useEffect(() => {
43-
if (
44-
isNullish(chartRoot) ||
45-
isNullish(stockChart) ||
46-
isNullish(mainPanel) ||
47-
isNullish(valueAxis) ||
48-
isNullish(dateAxis)
49-
) {
50-
console.error('ValueSeries should be used within StockAxis');
51-
return;
52-
}
49+
const newValueSeries = mainPanel.series.push(
50+
am5xy.CandlestickSeries.new(chartRoot, {
51+
name: seriesSettings?.name || 'MSFT',
52+
clustered: seriesSettings?.clustered || false,
53+
valueXField: 'Timestamp',
54+
valueYField: 'Close',
55+
highValueYField: 'High',
56+
lowValueYField: 'Low',
57+
openValueYField: 'Open',
58+
calculateAggregates: true,
59+
xAxis: dateAxis,
60+
yAxis: valueAxis,
61+
legendValueText:
62+
seriesSettings?.legendValueText ||
63+
'시작가: [bold]{openValueY}[/] 최고가: [bold]{highValueY}[/] 최저가: [bold]{lowValueY}[/] 종가: [bold]{valueY}[/]',
64+
legendRangeValueText: seriesSettings?.legendRangeValueText || '',
65+
}),
66+
);
5367

54-
if (isDisposed(chartRoot, stockChart, mainPanel, valueAxis, dateAxis))
55-
return;
68+
stockChart.set('stockSeries', newValueSeries);
69+
mainPanel.set(
70+
'cursor',
71+
am5xy.XYCursor.new(chartRoot, {
72+
yAxis: valueAxis,
73+
xAxis: dateAxis,
74+
snapToSeries: [newValueSeries],
75+
snapToSeriesBy: 'y!',
76+
}),
77+
);
5678

57-
const newValueSeries = mainPanel.series.push(
58-
am5xy.CandlestickSeries.new(chartRoot, {
59-
name: seriesSettings?.name || 'MSFT',
60-
clustered: seriesSettings?.clustered || false,
61-
valueXField: 'Timestamp',
62-
valueYField: 'Close',
63-
highValueYField: 'High',
64-
lowValueYField: 'Low',
65-
openValueYField: 'Open',
66-
calculateAggregates: true,
67-
xAxis: dateAxis,
68-
yAxis: valueAxis,
69-
legendValueText:
70-
seriesSettings?.legendValueText ||
71-
'시작가: [bold]{openValueY}[/] 최고가: [bold]{highValueY}[/] 최저가: [bold]{lowValueY}[/] 종가: [bold]{valueY}[/]',
72-
legendRangeValueText: seriesSettings?.legendRangeValueText || '',
73-
}),
74-
);
79+
const volumeSeries = mainPanel.series.push(
80+
am5xy.ColumnSeries.new(chartRoot, {
81+
name: 'Volume',
82+
valueXField: 'Timestamp',
83+
valueYField: 'Volume',
84+
xAxis: dateAxis,
85+
yAxis: valueAxis,
86+
legendValueText: '[bold]{valueY}',
87+
legendRangeValueText: '',
88+
}),
89+
);
7590

76-
stockChart.set('stockSeries', newValueSeries);
77-
mainPanel.set(
78-
'cursor',
79-
am5xy.XYCursor.new(chartRoot, {
80-
yAxis: valueAxis,
81-
xAxis: dateAxis,
82-
snapToSeries: [newValueSeries],
83-
snapToSeriesBy: 'y!',
84-
}),
85-
);
86-
setValueSeries(newValueSeries);
91+
const valueLegend = mainPanel.plotContainer.children.push(
92+
am5stock.StockLegend.new(chartRoot, {
93+
stockChart: stockChart,
94+
}),
95+
);
8796

88-
const volumeSeries = mainPanel.series.push(
89-
am5xy.ColumnSeries.new(chartRoot, {
90-
name: 'Volume',
91-
valueXField: 'Timestamp',
92-
valueYField: 'Volume',
93-
xAxis: dateAxis,
94-
yAxis: valueAxis,
95-
legendValueText: '[bold]{valueY}',
96-
legendRangeValueText: '',
97-
}),
98-
);
97+
valueLegend.data.setAll([newValueSeries]);
9998

100-
const valueLegend = mainPanel.plotContainer.children.push(
101-
am5stock.StockLegend.new(chartRoot, {
102-
stockChart: stockChart,
103-
}),
104-
);
99+
dateAxis.on('start', async (value) => {
100+
if (!value) return;
105101

106-
valueLegend.data.setAll([newValueSeries]);
102+
if (value < 0) {
103+
fetchPastTimeData?.();
104+
// dateAxis.zoom(0, 1, 0);
105+
}
106+
});
107107

108-
return () => {
109-
newValueSeries.dispose();
110-
valueLegend.dispose();
111-
};
112-
}, [valueAxis, mainPanel, stockChart, dateAxis, chartRoot, seriesSettings]);
108+
newValueSeries.data.clear();
109+
newValueSeries.data.setAll(pastTimeData || []);
113110

114111
return children;
115112
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useCallback, useState } from 'react';
2+
import type { CandlestickData } from '../../types/tradeview.type';
3+
import ValueSeries, { type ValueSeriesProps } from './ValueSeries';
4+
5+
const DUMMY_DATA: CandlestickData[] = (() => {
6+
const data: CandlestickData[] = [];
7+
const now = new Date();
8+
const basePrice = 50000; // 기준 가격
9+
const baseVolume = 1000; // 기준 거래량
10+
11+
// 최근 2시간 (120분)의 분봉 데이터
12+
for (let i = 119; i >= 0; i--) {
13+
const date = new Date();
14+
date.setMinutes(now.getMinutes() - i);
15+
16+
// 랜덤 변동폭 (이전 봉 종가의 -0.5%~0.5%)
17+
const changePercent = (Math.random() * 1 - 0.5) / 100;
18+
19+
// 시가는 이전 봉 종가에서 시작
20+
const open = data.length ? data[data.length - 1].Close : basePrice;
21+
22+
// 종가는 시가에서 랜덤 변동
23+
const close = open * (1 + changePercent);
24+
25+
// 고가는 시가와 종가 중 큰 값보다 0-0.5% 높게
26+
const highBaseValue = Math.max(open, close);
27+
const high = highBaseValue * (1 + Math.random() * 0.005);
28+
29+
// 저가는 시가와 종가 중 작은 값보다 0-0.5% 낮게
30+
const lowBaseValue = Math.min(open, close);
31+
const low = lowBaseValue * (1 - Math.random() * 0.005);
32+
33+
// 거래량은 기준 거래량의 50-150%, 분봉이므로 일봉보다 적게
34+
const volume = baseVolume * (0.5 + Math.random());
35+
36+
// 각 분 단위 타임스탬프 (ms)
37+
const timestamp = date.getTime();
38+
39+
data.push({
40+
Timestamp: timestamp,
41+
Open: Number(open.toFixed(2)),
42+
Close: Number(close.toFixed(2)),
43+
High: Number(high.toFixed(2)),
44+
Low: Number(low.toFixed(2)),
45+
Volume: Math.round(volume),
46+
});
47+
}
48+
49+
return data;
50+
})();
51+
52+
export default function ValueSeriesWithData(props: ValueSeriesProps) {
53+
const [data, setData] = useState<CandlestickData[]>(DUMMY_DATA);
54+
55+
const fetchPastTimeData = useCallback(async () => {
56+
setData((prevData) => {
57+
if (prevData.length === 0) return prevData;
58+
59+
// 기존 데이터의 가장 오래된 타임스탬프와 가격을 기준으로 함
60+
const oldestData = prevData[0];
61+
const oldestTimestamp = oldestData.Timestamp;
62+
const basePrice = oldestData.Open;
63+
const baseVolume = 1000;
64+
65+
const pastData: CandlestickData[] = [];
66+
67+
// 100개의 1분봉 데이터를 가장 오래된 데이터보다 이전 시간으로 생성
68+
for (let i = 10; i >= 1; i--) {
69+
const date = new Date(oldestTimestamp);
70+
date.setMinutes(date.getMinutes() - i);
71+
72+
// 이전 봉의 종가를 기준으로 랜덤 변동 (-1% ~ +1%)
73+
const changePercent = (Math.random() * 2 - 1) / 100;
74+
const prevClose = pastData.length
75+
? pastData[pastData.length - 1].Close
76+
: basePrice;
77+
78+
const open = prevClose;
79+
const close = open * (1 + changePercent);
80+
81+
// 고가는 시가와 종가 중 큰 값보다 0-1% 높게
82+
const highBaseValue = Math.max(open, close);
83+
const high = highBaseValue * (1 + Math.random() * 0.01);
84+
85+
// 저가는 시가와 종가 중 작은 값보다 0-1% 낮게
86+
const lowBaseValue = Math.min(open, close);
87+
const low = lowBaseValue * (1 - Math.random() * 0.01);
88+
89+
// 거래량 (기준 거래량의 30-200%)
90+
const volume = baseVolume * (0.3 + Math.random() * 1.7);
91+
92+
pastData.push({
93+
Timestamp: date.getTime(),
94+
Open: Number(open.toFixed(2)),
95+
Close: Number(close.toFixed(2)),
96+
High: Number(high.toFixed(2)),
97+
Low: Number(low.toFixed(2)),
98+
Volume: Math.round(volume),
99+
});
100+
}
101+
102+
// 새로운 과거 데이터를 기존 데이터 앞에 추가
103+
return [...pastData, ...prevData];
104+
});
105+
}, []);
106+
107+
return (
108+
<ValueSeries
109+
{...props}
110+
pastTimeData={data}
111+
fetchPastTimeData={fetchPastTimeData}
112+
/>
113+
);
114+
}

src/features/tradeview/ui/Chart/XScrollBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default function XScrollBar({
3333
'scrollbarX',
3434
am5xy.XYChartScrollbar.new(chartRoot, {
3535
orientation: 'horizontal',
36-
height: 50,
36+
height: 20,
3737
}),
3838
);
3939

0 commit comments

Comments
 (0)