(Android) 키움 REST API 쉽게 활용하기 - 조건검색 + 일봉차트 (3)
4. 일봉차트 작성
일봉차트는 MPAndroidChart 오픈소스를 이용합니다. MPAndroidChart 작성에 대해서는 KIS Open API 봉차트 만들기 포스팅에서 설명드렸는데요. 요약하면 아래와 같습니다. 선차트도 동일한 방식으로 이해하시면 됩니다.
- Candle Entry 작성
- Candle DataSet 생성
- Candle Data Object
(1) 일봉차트 Layout 작성
일봉차트는 FavoritesFragment에서 처리하기 때문에 Favorites Fragment 생성시 이미 만들어진 fragment_favorites.xml을 수정해 줍니다.
Layout은 이미지에서 보여드린 바와 같이 상단에 종목 관련 요약정보, 중간에는 CandleStick 차트, 하단에 거래량 바차트로 간단하게 구성해 줍니다.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/stockNameTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:textStyle="bold"
android:textSize="18sp"
android:text="종목명" />
<TextView
android:id="@+id/currentPriceTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:textSize="18sp"
android:text="현재가" />
<TextView
android:id="@+id/previousDayChangeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:layout_marginLeft="8dp"
android:text="전일대비" />
<TextView
android:id="@+id/fluctuationRateTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:textSize="18sp"
android:textColor="@color/red"
android:layout_marginLeft="8dp"
android:text="등락률" />
<TextView
android:id="@+id/volumeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:textSize="18sp"
android:layout_marginLeft="8dp"
android:text="거래량" />
</LinearLayout>
<com.github.mikephil.charting.charts.CombinedChart
android:id="@+id/candleStickChart"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_weight="1" />
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/volumeBarChart"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginTop="8dp" />
</LinearLayout>
(2) StockItem
먼저 주식일봉차트 조회요청할 종목코드는 SearchFragment에서 번들로 넘겨준 StockItem 데이터이므로 이를 수신해 줍니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.getString("stockCode")?.let {
currentStockCode = it
Log.d("FavoritesFragment", "전달받은 종목 코드: $currentStockCode")
}
arguments?.getSerializable("stockItem")?.let {
currentStockItem = it as StockItem
Log.d("FavoritesFragment", "전달받은 종목 정보: $currentStockItem")
}
}
(3) 일봉차트 Class 작성
DailyChart Package를 하나 만들어 주고, 주식일봉차트 조회요청 TR로 받아올 차트 데이터를 수신하기 위한 클래스를 만들어줍니다. DailyChartResponse 데이터클래스는 주식일봉차트 조회TR JSON Response에 맞게 상위 종목코드와 stk_dt_pole_chart
_qry List로 만들어 줍니다.
package com.example.kiwoomstock.DailyChart
import com.google.gson.annotations.SerializedName
data class DailyChartResponse(
@SerializedName("stk_cd") val stkCd: String,
@SerializedName("stk_dt_pole_chart_qry") val stkDtPoleChartQry: List<DailyChartData>?
)
data class DailyChartData(
@SerializedName("cur_prc") val currentPrice: String,
@SerializedName("trde_qty") val tradeQuantity: String,
@SerializedName("trde_prica") val tradePrice: String,
@SerializedName("dt") val date: String,
@SerializedName("open_pric") val openPrice: String,
@SerializedName("high_pric") val highPrice: String,
@SerializedName("low_pric") val lowPrice: String,
@SerializedName("upd_stkpc_tp") val updateStockPriceType: String?,
@SerializedName("upd_rt") val updateRate: String?,
@SerializedName("bic_inds_tp") val bigIndustryType: String?,
@SerializedName("sm_inds_tp") val smallIndustryType: String?,
@SerializedName("stk_infr") val stockInformation: String?,
@SerializedName("upd_stkpc_event") val updateStockPriceEvent: String?,
@SerializedName("pred_close_pric") val predictedClosingPrice: String?
)
(3) 주식일봉차트 조회요청
번들에서 받아온 종목코드와 기준일자는 오늘 날짜로 주식일봉차트 조회요청 Request를 보내서 일봉차트 데이터를 수신합니다. 키움 REST API에서는 한번에 주식일봉차트 데이터 600개를 보내줍니다. 필요한 만큼 짤라서 사용하면 될 것 같고 본 예제에서는 150개를 사용했습니다. 일봉차트 데이터는 최근일부터 순차적으로 보내주기 때문에 차트에 데이터를 일치시키기 위해서는 reversed시켜야 올바른 차트를 볼 수 있습니다. 이렇게 정리한 일봉차트 데이터를 dailyChartDataList에 담아서 setupchart를 호출합니다.
private var dailyChartDataList: List<DailyChartData> = emptyList() -> 선언부에 변수 선언
private fun requestDailyChartData(stockCode: String) {
val today = LocalDate.now().toString().replace("-", "")
val requestBody = gson.toJson(mapOf(
"stk_cd" to stockCode,
"base_dt" to today,
"upd_stkpc_tp" to "1"
)).toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
val request = Request.Builder()
.url("https://api.kiwoom.com/api/dostk/chart")
.post(requestBody)
.addHeader("Content-Type", "application/json;charset=UTF-8")
.addHeader("authorization", ACCESS_TOKEN)
.addHeader("cont-yn", "N")
.addHeader("next-key", "")
.addHeader("api-id", "ka10081")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("FavoritesFragment", "일봉 차트 데이터 요청 실패 ($stockCode): ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val responseBody = response.body?.string()
responseBody?.let {
try {
val dailyChartResponse = gson.fromJson(it, DailyChartResponse::class.java)
dailyChartResponse.stkDtPoleChartQry?.let { chartDataList ->
val reversedList = chartDataList.reversed()
dailyChartDataList = if (reversedList.size > 150) {
reversedList.takeLast(150)
} else {
reversedList
}
setupCombinedChart(candleStickChart, dailyChartDataList)
setupVolumeBarChart(volumeBarChart, dailyChartDataList)
} ?: Log.w("FavoritesFragment", "일봉 차트 데이터 없음 ($stockCode): stkDtPoleChartQry is null")
} catch (e: Exception) {
Log.e("FavoritesFragment", "일봉 차트 데이터 파싱 오류 ($stockCode): ${e.message}")
}
}
} else {
Log.e("FavoritesFragment", "일봉 차트 데이터 응답 실패 ($stockCode): ${response.code}")
}
}
})
}
(4) 차트 작성
MPAndroidChart에서 CandleStick과 Line 차트를 한 차트에 그리기 위해서는 CombinedChart를 사용합니다. 예제에서는 봉차트와 이평선, 볼린저밴드, 일목균형표를 포함하기 때문에 combined chart를 사용했습니다. 시가, 고가, 저가, 종가를 CandleStick Data에 넣고, 이평선, 볼린저밴드, 일목균형표 차트 데이터는 Line Data에 넣습니다. 그런 후 combinedData에 setting시켜 주면 됩니다.
combinedData.setData(CandleData(candleDataSet))
combinedData.setData(lineData)
1) 이동평균선
MovingAverage를 계산하는 Function을 만들어주고, 5,10,20,60 이평선을 계산하고 속성(Color 등)을 정의하여 lineDataSet에 넣어줍니다.
val lineData = LineData().apply {
addDataSet(createLineDataSet(calculateMovingAverage(dailyChartDataList, 5), Color.rgb(255, 20, 147), "MA5").apply { lineWidth = 2f }) // 5이평선: 분홍색 (DeepPink)
addDataSet(createLineDataSet(calculateMovingAverage(dailyChartDataList, 10), Color.rgb(135, 206, 235), "MA10")) // 10이평선: 하늘색 (SkyBlue)
addDataSet(createLineDataSet(calculateMovingAverage(dailyChartDataList, 20), Color.RED, "MA20").apply { lineWidth = 1.5f }) // 20이평선: 빨간색
addDataSet(createLineDataSet(calculateMovingAverage(dailyChartDataList, 60), Color.GREEN, "MA60")) // 60이평선: 녹색
val bollingerBands = calculateBollingerBands(dailyChartDataList, 20)
addDataSet(createLineDataSet(bollingerBands.middleBand, Color.RED, "볼린저 밴드 (중심선)").apply { lineWidth = 1.5f }) // 볼린저 밴드 (중심선): 빨간색
addDataSet(createLineDataSet(bollingerBands.upperBand, Color.RED, "볼린저 밴드 (상한선)").apply { lineWidth = 1f })
addDataSet(createLineDataSet(bollingerBands.lowerBand, Color.BLUE, "볼린저 밴드 (하한선)").apply { lineWidth = 1f })
val ichimokuData = calculateIchimoku(dailyChartDataList)
addDataSet(createLineDataSet(ichimokuData.conversionLine, Color.rgb(3, 169, 244), "전환선")) // 전환선: #03A9F4
addDataSet(createLineDataSet(ichimokuData.baseLine, Color.BLACK, "기준선")) // 기준선: 검은색
addDataSet(createLineDataSet(ichimokuData.laggingSpan, Color.GRAY, "후행 스팬"))
}
private fun calculateMovingAverage(dataList: List<DailyChartData>, period: Int): List<Entry> {
val movingAverage = mutableListOf<Entry>()
for (i in period - 1 until dataList.size) {
var sum = 0f
for (j in i - period + 1..i) {
sum += dataList[j].currentPrice.toFloat()
}
movingAverage.add(Entry(i.toFloat(), sum / period))
}
return movingAverage
}
2) 볼린저밴드
볼린저는 보통 20개의 평균과 표준편차를 이용하여 작성합니다. 평균을 중심으로 +2시그마(표준편차의 2배)가 상한, -2시그마가 하한선입니다. 그런데 키움증권 영웅문 S#에서 제공하는 볼린저밴드는 현재가, 고가, 저가 이렇게 3개의 평균값으로 계산해서 차트를 그려주지만, 종가로 그리는 것과 큰 차이는 없습니다. 증권사 앱의 차트와 맞추고 싶으면 아래 코드를 약간 수정해서 쓰시면 됩니다.
볼린저밴드 각각의 중심선, 상한선, 하한선 리스트를 만들어주고 계산된 값을 매핑해 준 후 BollingerBandsData 클래스에 저장하여 linedataset으로 Return해 줍니다.
private fun calculateBollingerBands(dataList: List<DailyChartData>, period: Int): BollingerBandsData {
val middleBandEntries = mutableListOf<Entry>()
val upperBandEntries = mutableListOf<Entry>()
val lowerBandEntries = mutableListOf<Entry>()
for (i in period - 1 until dataList.size) {
val subList = dataList.subList(i - period + 1, i + 1)
val prices = subList.map { it.currentPrice.toFloat() }
if (prices.isNotEmpty()) {
val average = prices.average().toFloat()
val standardDeviation = calculateStandardDeviation(prices, average)
middleBandEntries.add(Entry(i.toFloat(), average))
upperBandEntries.add(Entry(i.toFloat(), average + 2 * standardDeviation))
lowerBandEntries.add(Entry(i.toFloat(), average - 2 * standardDeviation))
}
}
return BollingerBandsData(middleBandEntries, upperBandEntries, lowerBandEntries)
}
data class BollingerBandsData(
val middleBand: List<Entry>,
val upperBand: List<Entry>,
val lowerBand: List<Entry>
)
3) 일목균형표(Ichimoku)
주가가 강세인 경우, 전환선 (9일간 주가의 저점과 고점의 중간값) 과 기준선 ( 26일간 주가의 저점과 고점의 중간값) 이 상승하면서 후행스팬은 주가영역을 침범하지 않고, 선행스팬은 양운을 드리우며 상승해 나가는 것을 일본의 일목산인이 시각화한 것이라고 하죠. 반대로 주가가 하락에서 상승으로 전환하는 경우, 위의 지표들이 강한 저항이 되므로, 어느 정도 위험관리를 할 수 있다고 합니다.
일봉차트 서두에서 말씀드렸듯이, 선행스팬 구름대 색칠하는 부분에 Rendering 이슈가 있어 그 부분은 제외합니다. 선행스팬에 관심있는 분은 별도로 스터디해 보시길 권장드립니다. 일목균형표 클래스를 하나 만들어줍니다.
data class IchimokuData(
val conversionLine: List<Entry>,
val baseLine: List<Entry>,
val leadingSpan1: List<Entry>,
val leadingSpan2: List<Entry>,
val laggingSpan: List<Entry>
)
주식일봉차트 조회로 받은 데이터로 위 지표의 Line을 계산해 주고, 볼린저밴드에서와 마찬가지로 IchimokuData에 저장해서 linedataset으로 Return해 줍니다.
private fun calculateIchimoku(dataList: List<DailyChartData>): IchimokuData {
val conversionLine = calculateConversionLine(dataList)
val baseLine = calculateBaseLine(dataList)
val laggingSpan = calculateLaggingSpan(dataList)
return IchimokuData(conversionLine, baseLine, emptyList(), emptyList(), laggingSpan)
}
private fun calculateConversionLine(dataList: List<DailyChartData>): List<Entry> {
val period = 9
val conversionLine = mutableListOf<Entry>()
for (i in period - 1 until dataList.size) {
val subList = dataList.subList(i - period + 1, i + 1)
val max = subList.maxOf { it.highPrice.toFloat() }
val min = subList.minOf { it.lowPrice.toFloat() }
conversionLine.add(Entry(i.toFloat(), (max + min) / 2))
}
return conversionLine
}
private fun calculateBaseLine(dataList: List<DailyChartData>): List<Entry> {
val period = 26
val baseLine = mutableListOf<Entry>()
for (i in period - 1 until dataList.size) {
val subList = dataList.subList(i - period + 1, i + 1)
val max = subList.maxOf { it.highPrice.toFloat() }
val min = subList.minOf { it.lowPrice.toFloat() }
baseLine.add(Entry(i.toFloat(), (max + min) / 2))
}
return baseLine
}
private fun calculateLaggingSpan(dataList: List<DailyChartData>): List<Entry> {
val laggingSpan = mutableListOf<Entry>()
for (i in 26 until dataList.size) {
laggingSpan.add(Entry((i - 26).toFloat(), dataList[i].currentPrice.toFloat()))
}
return laggingSpan
}
나머지 부분은 FavoritesFragment(일봉차트) 전체 소스코드를 첨부해 드립니다. ACCESS TOKEN은 본인이 발급받은 토큰을 사용하시면 됩니다.
이렇게 해서 조건검색 + 일봉차트 포스팅을 마칩니다. 궁금하신 부분이 있다면 댓글 달아 주시면 답변드리겠습니다.
package com.example.kiwoomstock
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.example.kiwoomstock.DailyChart.DailyChartData
import com.example.kiwoomstock.DailyChart.DailyChartResponse
import com.example.kiwoomstock.PSearch.StockItem
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.CombinedChart
import com.github.mikephil.charting.data.*
import com.github.mikephil.charting.formatter.IndexAxisValueFormatter
import com.github.mikephil.charting.formatter.ValueFormatter
import com.google.gson.Gson
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.text.DecimalFormat
import java.time.LocalDate
import java.util.concurrent.TimeUnit
import kotlin.math.sqrt
class FavoritesFragment : Fragment() {
private lateinit var stockNameTextView: TextView
private lateinit var currentPriceTextView: TextView
private lateinit var previousDayChangeTextView: TextView
private lateinit var fluctuationRateTextView: TextView
private lateinit var volumeTextView: TextView // 거래량 TextView 추가
private lateinit var candleStickChart: CombinedChart
private lateinit var volumeBarChart: BarChart
private val ACCESS_TOKEN = "Bearer 본인이 발급받으신 토큰"
private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build()
private val gson = Gson()
private val decimalFormat = DecimalFormat("#,###")
private var currentStockCode: String? = null
private var currentStockItem: StockItem? = null // StockItem 저장 변수
private var dailyChartDataList: List<DailyChartData> = emptyList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.getString("stockCode")?.let {
currentStockCode = it
Log.d("FavoritesFragment", "전달받은 종목 코드: $currentStockCode")
}
arguments?.getSerializable("stockItem")?.let {
currentStockItem = it as StockItem
Log.d("FavoritesFragment", "전달받은 종목 정보: $currentStockItem")
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_favorites, container, false)
stockNameTextView = view.findViewById(R.id.stockNameTextView)
currentPriceTextView = view.findViewById(R.id.currentPriceTextView)
previousDayChangeTextView = view.findViewById(R.id.previousDayChangeTextView)
fluctuationRateTextView = view.findViewById(R.id.fluctuationRateTextView)
volumeTextView = view.findViewById(R.id.volumeTextView) // 거래량 TextView 초기화
candleStickChart = view.findViewById(R.id.candleStickChart)
volumeBarChart = view.findViewById(R.id.volumeBarChart)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
currentStockItem?.let {
updateStockData(it) // StockItem으로 UI 업데이트
currentStockCode?.let { requestDailyChartData(it) } // 차트 데이터 요청
} ?: run {
currentStockCode?.let { requestDailyChartData(it) } // StockItem 없는 경우 (앱 처음 실행 등)
}
}
private fun updateStockData(stockItem: StockItem) {
stockNameTextView.text = stockItem.종목명 ?: "종목명 없음"
val currentPrice = stockItem.현재가.replace(",", "").toDoubleOrNull() ?: 0.0
val previousDayChange = stockItem.전일대비.replace(",", "").toDoubleOrNull() ?: 0.0
val tradeQuantity = stockItem.누적거래량.replace(",", "").toDoubleOrNull() ?: 0.0
val fluctuationRate = stockItem.등락률 // 이미 등락률 형태의 문자열
currentPriceTextView.text = decimalFormat.format(currentPrice.toInt())
previousDayChangeTextView.text = decimalFormat.format(previousDayChange.toInt())
fluctuationRateTextView.text = fluctuationRate // 넘어온 등락률 그대로 사용
volumeTextView.text = decimalFormat.format(tradeQuantity.toInt())
Log.d("FavoritesFragment", "현재가: $currentPrice, 전일 대비: $previousDayChange, 등락률: $fluctuationRate, 거래량: $tradeQuantity")
}
private fun requestDailyChartData(stockCode: String) {
val today = LocalDate.now().toString().replace("-", "")
val requestBody = gson.toJson(mapOf(
"stk_cd" to stockCode,
"base_dt" to today,
"upd_stkpc_tp" to "1"
)).toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
val request = Request.Builder()
.url("https://api.kiwoom.com/api/dostk/chart")
.post(requestBody)
.addHeader("Content-Type", "application/json;charset=UTF-8")
.addHeader("authorization", ACCESS_TOKEN)
.addHeader("cont-yn", "N")
.addHeader("next-key", "")
.addHeader("api-id", "ka10081")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("FavoritesFragment", "일봉 차트 데이터 요청 실패 ($stockCode): ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val responseBody = response.body?.string()
responseBody?.let {
try {
val dailyChartResponse = gson.fromJson(it, DailyChartResponse::class.java)
dailyChartResponse.stkDtPoleChartQry?.let { chartDataList ->
val reversedList = chartDataList.reversed()
dailyChartDataList = if (reversedList.size > 150) {
reversedList.takeLast(150)
} else {
reversedList
}
setupCombinedChart(candleStickChart, dailyChartDataList)
setupVolumeBarChart(volumeBarChart, dailyChartDataList)
} ?: Log.w("FavoritesFragment", "일봉 차트 데이터 없음 ($stockCode): stkDtPoleChartQry is null")
} catch (e: Exception) {
Log.e("FavoritesFragment", "일봉 차트 데이터 파싱 오류 ($stockCode): ${e.message}")
}
}
} else {
Log.e("FavoritesFragment", "일봉 차트 데이터 응답 실패 ($stockCode): ${response.code}")
}
}
})
}
private fun setupCombinedChart(chart: CombinedChart, dailyChartDataList: List<DailyChartData>) {
val combinedData = CombinedData()
val candleEntries = dailyChartDataList.mapIndexed { index, data ->
CandleEntry(
index.toFloat(),
data.highPrice.toFloat(),
data.lowPrice.toFloat(),
data.openPrice.toFloat(),
data.currentPrice.toFloat()
)
}
val candleDataSet = CandleDataSet(candleEntries, "일봉 데이터").apply {
shadowWidth = 0.7f
shadowColorSameAsCandle = true // 몸통 색과 그림자 색을 동일하게
// 상승 (종가가 시가보다 높음)
increasingColor = Color.RED
increasingPaintStyle = android.graphics.Paint.Style.FILL
// 하락 (종가가 시가보다 낮음)
decreasingColor = Color.BLUE
decreasingPaintStyle = android.graphics.Paint.Style.FILL
neutralColor = Color.LTGRAY // 시가와 종가가 같은 경우
}
combinedData.setData(CandleData(candleDataSet))
val lineData = LineData().apply {
addDataSet(createLineDataSet(calculateMovingAverage(dailyChartDataList, 5), Color.rgb(255, 20, 147), "MA5").apply { lineWidth = 2f }) // 5이평선: 분홍색 (DeepPink)
addDataSet(createLineDataSet(calculateMovingAverage(dailyChartDataList, 10), Color.rgb(135, 206, 235), "MA10")) // 10이평선: 하늘색 (SkyBlue)
addDataSet(createLineDataSet(calculateMovingAverage(dailyChartDataList, 20), Color.RED, "MA20").apply { lineWidth = 1.5f }) // 20이평선: 빨간색
addDataSet(createLineDataSet(calculateMovingAverage(dailyChartDataList, 60), Color.GREEN, "MA60")) // 60이평선: 녹색
val bollingerBands = calculateBollingerBands(dailyChartDataList, 20)
addDataSet(createLineDataSet(bollingerBands.middleBand, Color.RED, "볼린저 밴드 (중심선)").apply { lineWidth = 1.5f }) // 볼린저 밴드 (중심선): 빨간색
addDataSet(createLineDataSet(bollingerBands.upperBand, Color.RED, "볼린저 밴드 (상한선)").apply { lineWidth = 1f })
addDataSet(createLineDataSet(bollingerBands.lowerBand, Color.BLUE, "볼린저 밴드 (하한선)").apply { lineWidth = 1f })
val ichimokuData = calculateIchimoku(dailyChartDataList)
addDataSet(createLineDataSet(ichimokuData.conversionLine, Color.rgb(3, 169, 244), "전환선")) // 전환선: #03A9F4
addDataSet(createLineDataSet(ichimokuData.baseLine, Color.BLACK, "기준선")) // 기준선: 검은색
addDataSet(createLineDataSet(ichimokuData.laggingSpan, Color.GRAY, "후행 스팬"))
}
combinedData.setData(lineData)
chart.data = combinedData
chart.xAxis.valueFormatter = object : IndexAxisValueFormatter() {
override fun getFormattedValue(value: Float): String {
val index = value.toInt()
if (index >= 0 && index < dailyChartDataList.size) {
return dailyChartDataList[index].date.substring(2) // YYMMDD 형식으로 간략하게 표시
}
return ""
}
}
chart.xAxis.setDrawGridLines(false)
chart.xAxis.setPosition(com.github.mikephil.charting.components.XAxis.XAxisPosition.BOTTOM)
chart.axisRight.isEnabled = false
chart.legend.isEnabled = false
chart.description.isEnabled = false
chart.setTouchEnabled(true)
chart.isDragEnabled = true
chart.setScaleEnabled(true)
chart.setPinchZoom(true)
chart.setDrawBorders(true) // 테두리 라인 활성화
chart.setBorderColor(Color.BLACK) // 테두리 라인 색상 설정 (선택 사항)
chart.setBorderWidth(0.1f) // 테두리 라인 두께 설정 (선택 사항)
chart.legend.isEnabled = true // 범례 표시 활성화
chart.legend.setDrawInside(false)
chart.legend.isWordWrapEnabled = true
if (dailyChartDataList.isNotEmpty()) {
val dataCount = dailyChartDataList.size
val visibleCount = 60f // 한 번에 보여줄 데이터 개수
chart.setVisibleXRangeMaximum(visibleCount)
chart.moveViewToX(dataCount.toFloat() - 1)
val scaleX = if (dataCount > visibleCount) visibleCount / dataCount else 1f
chart.zoom(scaleX, 1f, dataCount.toFloat() - 1, 0f)
}
chart.invalidate()
}
private fun setupVolumeBarChart(barChart: BarChart, dailyChartDataList: List<DailyChartData>) {
val entries = dailyChartDataList.mapIndexed { index, data ->
BarEntry(index.toFloat(), data.tradeQuantity.toFloat())
}
val barDataSet = BarDataSet(entries, "거래량").apply {
color = Color.rgb(218, 165, 32) // 짙은 노란색 (Goldenrod)
setDrawValues(false)
}
val barData = BarData(barDataSet)
barChart.data = barData
barChart.xAxis.valueFormatter = object : IndexAxisValueFormatter() {
override fun getFormattedValue(value: Float): String {
val index = value.toInt()
if (index >= 0 && index < dailyChartDataList.size) {
return dailyChartDataList[index].date.substring(2) // YYMMDD 형식으로 간략하게 표시
}
return ""
}
}
barChart.xAxis.setDrawGridLines(false)
barChart.xAxis.setDrawLabels(true)
barChart.xAxis.setPosition(com.github.mikephil.charting.components.XAxis.XAxisPosition.BOTTOM)
barChart.xAxis.labelRotationAngle = 0f
barChart.axisLeft.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
if (value >= 1000) {
val thousands = value / 1000
return String.format("%.0fK", thousands)
}
return value.toInt().toString()
}
}
barChart.axisLeft.isEnabled = true
barChart.axisRight.isEnabled = false
barChart.description.isEnabled = false
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.setTouchEnabled(false)
barChart.isDragEnabled = false
barChart.isHighlightPerDragEnabled = false
barChart.isHighlightPerTapEnabled = false
barChart.invalidate()
}
private fun calculateMovingAverage(dataList: List<DailyChartData>, period: Int): List<Entry> {
val movingAverage = mutableListOf<Entry>()
for (i in period - 1 until dataList.size) {
var sum = 0f
for (j in i - period + 1..i) {
sum += dataList[j].currentPrice.toFloat()
}
movingAverage.add(Entry(i.toFloat(), sum / period))
}
return movingAverage
}
private fun calculateBollingerBands(dataList: List<DailyChartData>, period: Int): BollingerBandsData {
val middleBandEntries = mutableListOf<Entry>()
val upperBandEntries = mutableListOf<Entry>()
val lowerBandEntries = mutableListOf<Entry>()
for (i in period - 1 until dataList.size) {
val subList = dataList.subList(i - period + 1, i + 1)
val prices = subList.map { it.currentPrice.toFloat() }
if (prices.isNotEmpty()) {
val average = prices.average().toFloat()
val standardDeviation = calculateStandardDeviation(prices, average)
middleBandEntries.add(Entry(i.toFloat(), average))
upperBandEntries.add(Entry(i.toFloat(), average + 2 * standardDeviation))
lowerBandEntries.add(Entry(i.toFloat(), average - 2 * standardDeviation))
}
}
return BollingerBandsData(middleBandEntries, upperBandEntries, lowerBandEntries)
}
private fun calculateStandardDeviation(data: List<Float>, average: Float): Float {
var sumOfSquares = 0f
for (value in data) {
sumOfSquares += (value - average) * (value - average)
}
return if (data.size > 1) sqrt(sumOfSquares / (data.size - 1)) else 0f
}
private fun createLineDataSet(entries: List<Entry>, color: Int, label: String): LineDataSet {
return LineDataSet(entries, label).apply {
this.color = color
lineWidth = 1f
setDrawCircles(false)
setDrawValues(false)
}
}
private fun calculateIchimoku(dataList: List<DailyChartData>): IchimokuData {
val conversionLine = calculateConversionLine(dataList)
val baseLine = calculateBaseLine(dataList)
val laggingSpan = calculateLaggingSpan(dataList)
return IchimokuData(conversionLine, baseLine, emptyList(), emptyList(), laggingSpan)
}
private fun calculateConversionLine(dataList: List<DailyChartData>): List<Entry> {
val period = 9
val conversionLine = mutableListOf<Entry>()
for (i in period - 1 until dataList.size) {
val subList = dataList.subList(i - period + 1, i + 1)
val max = subList.maxOf { it.highPrice.toFloat() }
val min = subList.minOf { it.lowPrice.toFloat() }
conversionLine.add(Entry(i.toFloat(), (max + min) / 2))
}
return conversionLine
}
private fun calculateBaseLine(dataList: List<DailyChartData>): List<Entry> {
val period = 26
val baseLine = mutableListOf<Entry>()
for (i in period - 1 until dataList.size) {
val subList = dataList.subList(i - period + 1, i + 1)
val max = subList.maxOf { it.highPrice.toFloat() }
val min = subList.minOf { it.lowPrice.toFloat() }
baseLine.add(Entry(i.toFloat(), (max + min) / 2))
}
return baseLine
}
private fun calculateLaggingSpan(dataList: List<DailyChartData>): List<Entry> {
val laggingSpan = mutableListOf<Entry>()
for (i in 26 until dataList.size) {
laggingSpan.add(Entry((i - 26).toFloat(), dataList[i].currentPrice.toFloat()))
}
return laggingSpan
}
data class IchimokuData(
val conversionLine: List<Entry>,
val baseLine: List<Entry>,
val leadingSpan1: List<Entry>,
val leadingSpan2: List<Entry>,
val laggingSpan: List<Entry>
)
data class BollingerBandsData(
val middleBand: List<Entry>,
val upperBand: List<Entry>,
val lowerBand: List<Entry>
)
}