2026-01-10 18:16:50 +09:00
|
|
|
package network
|
|
|
|
|
|
|
|
|
|
import io.ktor.client.*
|
|
|
|
|
import io.ktor.client.call.*
|
|
|
|
|
import io.ktor.client.engine.cio.CIO
|
|
|
|
|
import io.ktor.client.plugins.contentnegotiation.*
|
|
|
|
|
import io.ktor.client.plugins.logging.DEFAULT
|
|
|
|
|
import io.ktor.client.plugins.logging.LogLevel
|
|
|
|
|
import io.ktor.client.plugins.logging.Logger
|
|
|
|
|
import io.ktor.client.plugins.logging.Logging
|
|
|
|
|
import io.ktor.client.request.*
|
|
|
|
|
import io.ktor.http.*
|
|
|
|
|
import io.ktor.serialization.kotlinx.json.*
|
|
|
|
|
import kotlinx.serialization.json.Json
|
|
|
|
|
import model.CandleData
|
|
|
|
|
import model.RankingResponse
|
|
|
|
|
import model.RankingStock
|
|
|
|
|
import model.RankingType
|
|
|
|
|
import model.StockBalanceResponse
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
import kotlinx.coroutines.async
|
|
|
|
|
import kotlinx.coroutines.coroutineScope
|
|
|
|
|
import kotlinx.serialization.json.JsonObject
|
|
|
|
|
import kotlinx.serialization.json.jsonArray
|
|
|
|
|
import kotlinx.serialization.json.jsonObject
|
|
|
|
|
import kotlinx.serialization.json.jsonPrimitive
|
|
|
|
|
import model.*
|
|
|
|
|
|
|
|
|
|
class KisTradeService {
|
2026-01-10 18:16:50 +09:00
|
|
|
private val client = HttpClient(CIO) {
|
|
|
|
|
install(ContentNegotiation) {
|
2026-01-13 16:04:25 +09:00
|
|
|
json(Json {
|
|
|
|
|
ignoreUnknownKeys = true
|
|
|
|
|
encodeDefaults = true // 기본값이 포함된 요청 바디를 정확히 전송하기 위해 필요
|
|
|
|
|
})
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
// [수정] 모든 로그(Headers + Body)를 찍도록 설정
|
2026-01-10 18:16:50 +09:00
|
|
|
install(Logging) {
|
|
|
|
|
logger = Logger.DEFAULT
|
2026-01-13 16:04:25 +09:00
|
|
|
level = LogLevel.ALL // 상세한 디버깅을 위해 ALL로 변경
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
private val prodUrl = "https://openapi.koreainvestment.com:9443"
|
|
|
|
|
private val vtsUrl = "https://openapivts.koreainvestment.com:29443"
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
/**
|
|
|
|
|
* [1] 통합 잔고 조회 (국내 + 해외 합산)
|
|
|
|
|
*/
|
|
|
|
|
suspend fun fetchIntegratedBalance(): Result<UnifiedBalance> = coroutineScope {
|
|
|
|
|
val config = KisSession.config
|
|
|
|
|
|
|
|
|
|
// 국내와 해외 잔고를 비동기로 동시 호출
|
|
|
|
|
val domesticJob = async { fetchDomesticRawBalance() }
|
|
|
|
|
val overseasJob = async { fetchOverseasRawBalance() }
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
val domRes = domesticJob.await().getOrNull()
|
|
|
|
|
val ovsRes = overseasJob.await().getOrNull()
|
|
|
|
|
|
|
|
|
|
val combinedHoldings = mutableListOf<UnifiedStockHolding>()
|
|
|
|
|
|
|
|
|
|
// 국내 종목 매핑
|
|
|
|
|
domRes?.output1?.forEach {
|
|
|
|
|
combinedHoldings.add(UnifiedStockHolding(
|
|
|
|
|
code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty,
|
|
|
|
|
avgPrice = it.pchs_avg_pric, currentPrice = it.prpr,
|
|
|
|
|
profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = true
|
|
|
|
|
))
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
// 해외 종목 매핑 (해외 API 응답 모델 구조에 따라 필드 매핑)
|
|
|
|
|
ovsRes?.output1?.forEach {
|
|
|
|
|
combinedHoldings.add(UnifiedStockHolding(
|
|
|
|
|
code = it.pdno, name = it.prdt_name, quantity = it.hldg_qty,
|
|
|
|
|
avgPrice = it.pchs_avg_pric, currentPrice = it.prpr,
|
|
|
|
|
profitRate = it.evlu_pfls_rt, evalAmount = it.evlu_amt, isDomestic = false
|
|
|
|
|
))
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
val totalAmt = (domRes?.output2?.firstOrNull()?.tot_evlu_amt?.toLongOrNull() ?: 0L) +
|
|
|
|
|
(ovsRes?.output2?.firstOrNull()?.tot_evlu_amt?.toLongOrNull() ?: 0L)
|
|
|
|
|
|
|
|
|
|
Result.success(UnifiedBalance(
|
|
|
|
|
totalAsset = String.format("%,d", totalAmt),
|
|
|
|
|
totalProfitRate = domRes?.output2?.firstOrNull()?.evlu_pfls_rt ?: "0.0",
|
|
|
|
|
holdings = combinedHoldings
|
|
|
|
|
))
|
|
|
|
|
} catch (e: Exception) { Result.failure(e) }
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-13 16:04:25 +09:00
|
|
|
* [통합 순위 조회] 국내/해외 분기 처리
|
2026-01-10 18:16:50 +09:00
|
|
|
*/
|
2026-01-13 16:04:25 +09:00
|
|
|
suspend fun fetchMarketRanking(type: RankingType, isDomestic: Boolean): Result<List<RankingStock>> {
|
|
|
|
|
return if (isDomestic) {
|
|
|
|
|
fetchDomesticRanking(type)
|
|
|
|
|
} else {
|
|
|
|
|
fetchOverseasRanking(type)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
/**
|
|
|
|
|
* [국내 주식 순위] 명세서 기반 파라미터 최적화
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun fetchDomesticRanking(type: RankingType): Result<List<RankingStock>> {
|
|
|
|
|
val config = KisSession.config
|
2026-01-10 18:16:50 +09:00
|
|
|
return try {
|
2026-01-13 16:04:25 +09:00
|
|
|
val response = client.get("$prodUrl${type.path}") {
|
|
|
|
|
header("authorization", "Bearer ${config.marketToken}")
|
|
|
|
|
header("appkey", config.realAppKey)
|
|
|
|
|
header("appsecret", config.realSecretKey)
|
|
|
|
|
header("tr_id", type.trId)
|
2026-01-10 18:16:50 +09:00
|
|
|
header("custtype", "P")
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
parameter("FID_COND_MRKT_DIV_CODE", "J")
|
|
|
|
|
parameter("FID_COND_SCR_DIV_CODE", type.scrNo)
|
|
|
|
|
parameter("FID_INPUT_ISCD", "0000") // 전체 시장
|
|
|
|
|
parameter("FID_DIV_CLS_CODE", "0") // 전체
|
|
|
|
|
|
|
|
|
|
parameter("FID_ETC_CLS_CODE", "0")
|
|
|
|
|
parameter("FID_PRC_CLS_CODE", "0")
|
|
|
|
|
when(type) {
|
|
|
|
|
RankingType.VALUE -> {
|
|
|
|
|
parameter("FID_BLNG_CLS_CODE", type.sortCode)
|
|
|
|
|
}
|
|
|
|
|
RankingType.VOLUME -> {
|
|
|
|
|
parameter("FID_BLNG_CLS_CODE",type.sortCode)
|
|
|
|
|
}
|
|
|
|
|
RankingType.FALL -> {
|
|
|
|
|
parameter("FID_RANK_SORT_CLS_CODE", type.sortCode)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
RankingType.RISE -> {
|
|
|
|
|
parameter("FID_RANK_SORT_CLS_CODE", type.sortCode)
|
|
|
|
|
}
|
|
|
|
|
// RankingType.AFTER -> {
|
|
|
|
|
// parameter("FID_MKOP_CLS_CODE", type.sortCode)
|
|
|
|
|
// }
|
|
|
|
|
// RankingType.BEFORE -> {
|
|
|
|
|
// parameter("FID_MKOP_CLS_CODE", type.sortCode)
|
|
|
|
|
// }
|
|
|
|
|
else -> {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
parameter("FID_PBMN", "")
|
|
|
|
|
parameter("FID_APLY_RANG_PRC_1", "")
|
|
|
|
|
parameter("FID_TRGT_CLS_CODE", "11111111")
|
|
|
|
|
parameter("FID_TRGT_EXLS_CLS_CODE", "000000")
|
|
|
|
|
parameter("FID_RSFL_RATE2", "")
|
|
|
|
|
parameter("FID_RSFL_RATE1", "")
|
|
|
|
|
parameter("FID_INPUT_CNT_1", "0")
|
|
|
|
|
parameter("FID_INPUT_PRICE_1", "")
|
|
|
|
|
parameter("FID_INPUT_PRICE_2", "")
|
|
|
|
|
parameter("FID_VOL_CNT", "")
|
|
|
|
|
parameter("FID_INPUT_DATE_1", "")
|
2026-01-10 18:16:50 +09:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
|
|
|
|
|
// 상승/하락률 순위(HHPST01710000)일 경우 추가 파라미터
|
|
|
|
|
if (type.trId == "HHPST01710000") {
|
|
|
|
|
parameter("fid_diff_div_code", "00") // 00: 전일 대비
|
|
|
|
|
}
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
val body = response.body<RankingResponse>()
|
|
|
|
|
if (body.rt_cd == "0") Result.success(body.list) else Result.failure(Exception(body.msg1))
|
|
|
|
|
} catch (e: Exception) { Result.failure(e) }
|
|
|
|
|
}
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
/**
|
|
|
|
|
* [해외 주식 순위] 모델 매핑 오류 수정
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun fetchOverseasRanking(type: RankingType): Result<List<RankingStock>> {
|
|
|
|
|
val config = KisSession.config
|
|
|
|
|
val path = "/uapi/overseas-stock/v1/quotations/rank-fluctuation"
|
|
|
|
|
val trId = "HHDFS76240000"
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
return try {
|
|
|
|
|
val response = client.get("$prodUrl$path") {
|
|
|
|
|
header("authorization", "Bearer ${config.marketToken}")
|
|
|
|
|
header("appkey", config.realAppKey)
|
|
|
|
|
header("appsecret", config.realSecretKey)
|
2026-01-10 18:16:50 +09:00
|
|
|
header("tr_id", trId)
|
|
|
|
|
header("custtype", "P")
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
parameter("EXCD", "NAS") // 기본 나스닥
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
val gubn = when (type) {
|
|
|
|
|
RankingType.RISE -> "0"
|
|
|
|
|
RankingType.FALL -> "1"
|
|
|
|
|
RankingType.VOLUME -> "2"
|
|
|
|
|
RankingType.VALUE -> "3"
|
|
|
|
|
else -> "0"
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
parameter("GUBN", gubn)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// [수정] OverseasRankingResponse로 정확히 파싱 후 변환
|
|
|
|
|
val body = response.body<OverseasRankingResponse>()
|
|
|
|
|
if (body.rt_cd == "0") {
|
|
|
|
|
Result.success(body.output.map { it.toRankingStock() })
|
2026-01-10 18:16:50 +09:00
|
|
|
} else {
|
2026-01-13 16:04:25 +09:00
|
|
|
Result.failure(Exception("해외 랭킹 에러: ${body.msg1}"))
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
} catch (e: Exception) { Result.failure(e) }
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
/**
|
|
|
|
|
* [3] 통합 주문 (지정가/시장가 매수/매도)
|
|
|
|
|
*/
|
|
|
|
|
suspend fun postOrder(
|
|
|
|
|
stockCode: String,
|
|
|
|
|
qty: String,
|
|
|
|
|
price: String, // "0" 이면 시장가
|
|
|
|
|
isBuy: Boolean
|
|
|
|
|
): Result<String> {
|
|
|
|
|
val config = KisSession.config
|
|
|
|
|
val isDomestic = stockCode.length == 6 && stockCode.all { it.isDigit() }
|
|
|
|
|
val baseUrl = if (config.isSimulation) vtsUrl else prodUrl
|
|
|
|
|
|
|
|
|
|
val trId = when {
|
|
|
|
|
isDomestic && config.isSimulation -> if (isBuy) "VTRP0001U" else "VTRP0002U"
|
|
|
|
|
isDomestic && !config.isSimulation -> if (isBuy) "TTTC0802U" else "TTTC0801U"
|
|
|
|
|
!isDomestic && config.isSimulation -> if (isBuy) "VTTT3001U" else "VTTT3002U"
|
|
|
|
|
else -> if (isBuy) "TTTS3001U" else "TTTS3002U"
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 18:16:50 +09:00
|
|
|
return try {
|
2026-01-13 16:04:25 +09:00
|
|
|
val response = client.post("$baseUrl/uapi/${if(isDomestic) "domestic" else "overseas"}-stock/v1/trading/order-cash") {
|
|
|
|
|
header("authorization", "Bearer ${config.tradeToken}")
|
|
|
|
|
header("appkey", if (config.isSimulation) config.vtsAppKey else config.realAppKey)
|
|
|
|
|
header("appsecret", if (config.isSimulation) config.vtsSecretKey else config.realSecretKey)
|
|
|
|
|
header("tr_id", trId)
|
|
|
|
|
header("Content-Type", "application/json")
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
setBody(mapOf(
|
|
|
|
|
"CANO" to config.accountNo.take(8),
|
|
|
|
|
"ACNT_PRDT_CD" to config.accountNo.takeLast(2),
|
|
|
|
|
"PDNO" to stockCode,
|
|
|
|
|
"ORD_DVSN" to if (price == "0") "01" else "00",
|
|
|
|
|
"ORD_QTY" to qty,
|
|
|
|
|
"ORD_UNPR" to price
|
|
|
|
|
))
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
val body = response.body<Map<String, Any>>()
|
|
|
|
|
if (body["rt_cd"] == "0") Result.success("✅ 주문 성공: ${body["msg1"]}")
|
|
|
|
|
else Result.failure(Exception("${body["msg1"]}"))
|
|
|
|
|
} catch (e: Exception) { Result.failure(e) }
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
/**
|
|
|
|
|
* [4] 웹소켓 승인키(Approval Key) 발급
|
|
|
|
|
*/
|
|
|
|
|
suspend fun refreshWebsocketKey(): Boolean {
|
|
|
|
|
val config = KisSession.config
|
2026-01-10 18:16:50 +09:00
|
|
|
return try {
|
2026-01-13 16:04:25 +09:00
|
|
|
val response = client.post("$prodUrl/oauth2/Approval") {
|
2026-01-10 18:16:50 +09:00
|
|
|
header("Content-Type", "application/json")
|
2026-01-13 16:04:25 +09:00
|
|
|
setBody(mapOf("grant_type" to "client_credentials", "appkey" to config.realAppKey, "secretkey" to config.realSecretKey))
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
if (response.status == HttpStatusCode.OK) {
|
|
|
|
|
val approvalKey = response.body<Map<String, String>>()["approval_key"]
|
|
|
|
|
if (approvalKey != null) {
|
|
|
|
|
KisSession.config = KisSession.config.copy(websocketToken = approvalKey)
|
|
|
|
|
true
|
|
|
|
|
} else false
|
|
|
|
|
} else false
|
|
|
|
|
} catch (e: Exception) { false }
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
/**
|
|
|
|
|
* [5] 차트 데이터 조회 (일봉 기준)
|
|
|
|
|
*/
|
|
|
|
|
suspend fun fetchChartData(stockCode: String, isDomestic: Boolean): Result<List<CandleData>> {
|
|
|
|
|
val config = KisSession.config
|
|
|
|
|
// 국내 주식 분봉 조회 TR ID: FHKST03010200
|
|
|
|
|
val trId = if (isDomestic) "FHKST03010200" else "HHDFS76240000"
|
|
|
|
|
val path = if (isDomestic)
|
|
|
|
|
"/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice"
|
|
|
|
|
else "/uapi/overseas-stock/v1/quotations/inquire-time-itemchartprice"
|
|
|
|
|
|
2026-01-10 18:16:50 +09:00
|
|
|
return try {
|
2026-01-13 16:04:25 +09:00
|
|
|
val response = client.get("$prodUrl$path") {
|
|
|
|
|
header("authorization", "Bearer ${config.marketToken}")
|
|
|
|
|
header("appkey", config.realAppKey)
|
|
|
|
|
header("appsecret", config.realSecretKey)
|
|
|
|
|
header("tr_id", trId)
|
2026-01-10 18:16:50 +09:00
|
|
|
header("custtype", "P")
|
2026-01-13 16:04:25 +09:00
|
|
|
header("content-type", "application/json; charset=utf-8")
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
parameter("FID_ETC_CLS_CODE", "")
|
|
|
|
|
parameter("FID_COND_MRKT_DIV_CODE", "J")
|
|
|
|
|
parameter("FID_INPUT_ISCD", stockCode)
|
|
|
|
|
parameter("FID_INPUT_HOUR_1", "153000") // 장 마감 시간까지
|
|
|
|
|
parameter("FID_PW_DATA_INCU_YN", "Y") // 전일 데이터 포함 여부
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
// API 응답에서 output2(캔들 리스트)를 CandleData로 변환 (역순으로 오므로 reverse 필요)
|
|
|
|
|
val body = response.body<JsonObject>()
|
|
|
|
|
val output2 = body["output2"]?.jsonArray
|
|
|
|
|
|
|
|
|
|
val candles = output2?.map { element ->
|
|
|
|
|
val obj = element.jsonObject
|
2026-01-10 18:16:50 +09:00
|
|
|
CandleData(
|
2026-01-13 16:04:25 +09:00
|
|
|
stck_bsop_date = obj["stck_bsop_date"]?.jsonPrimitive?.content ?: "",
|
|
|
|
|
stck_clpr = obj["stck_prpr"]?.jsonPrimitive?.content ?: "0", // 분봉/시간 데이터는 stck_prpr이 종가
|
|
|
|
|
stck_oprc = obj["stck_oprc"]?.jsonPrimitive?.content ?: "0",
|
|
|
|
|
stck_hgpr = obj["stck_hgpr"]?.jsonPrimitive?.content ?: "0",
|
|
|
|
|
stck_lwpr = obj["stck_lwpr"]?.jsonPrimitive?.content ?: "0",
|
|
|
|
|
acml_vol = obj["cntg_vol"]?.jsonPrimitive?.content ?: "0" // 필수 필드 누락 방지
|
2026-01-10 18:16:50 +09:00
|
|
|
)
|
2026-01-13 16:04:25 +09:00
|
|
|
}?.reversed() ?: emptyList()
|
|
|
|
|
|
|
|
|
|
Result.success(candles)
|
|
|
|
|
} catch (e: Exception) { Result.failure(e) }
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
// --- 내부 Raw 호출용 (통합 잔고에서 사용) ---
|
|
|
|
|
private suspend fun fetchDomesticRawBalance(): Result<StockBalanceResponse> {
|
|
|
|
|
val config = KisSession.config
|
|
|
|
|
val baseUrl = if (config.isSimulation) vtsUrl else prodUrl
|
|
|
|
|
val trId = if (config.isSimulation) "VTTC8434R" else "TTTC8434R"
|
2026-01-10 18:16:50 +09:00
|
|
|
return try {
|
2026-01-13 16:04:25 +09:00
|
|
|
val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-balance") {
|
|
|
|
|
header("authorization", "Bearer ${config.tradeToken}")
|
|
|
|
|
header("appkey", if (config.isSimulation) config.vtsAppKey else config.realAppKey)
|
|
|
|
|
header("appsecret", if (config.isSimulation) config.vtsSecretKey else config.realSecretKey)
|
2026-01-10 18:16:50 +09:00
|
|
|
header("tr_id", trId)
|
2026-01-13 16:04:25 +09:00
|
|
|
parameter("CANO", config.accountNo.take(8))
|
|
|
|
|
parameter("ACNT_PRDT_CD", config.accountNo.takeLast(2))
|
|
|
|
|
parameter("AFHR_FLPR_YN", "N")
|
|
|
|
|
parameter("OFL_YN", "N")
|
|
|
|
|
parameter("INQR_DVSN", "02")
|
|
|
|
|
parameter("UNPR_DVSN", "01")
|
|
|
|
|
parameter("FUND_STTL_ICLD_YN", "N")
|
|
|
|
|
parameter("FNCG_AMT_AUTO_RDPT_YN", "N")
|
|
|
|
|
parameter("PRCS_DVSN", "00")
|
|
|
|
|
parameter("CTX_AREA_FK100", "")
|
|
|
|
|
parameter("CTX_AREA_NK100", "")
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
Result.success(response.body())
|
|
|
|
|
} catch (e: Exception) { Result.failure(e) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun fetchOverseasRawBalance(): Result<StockBalanceResponse> {
|
|
|
|
|
// 해외 잔고 조회 API 명세에 맞춰 구현 (국내와 유사하나 TR ID 및 파라미터 다름)
|
|
|
|
|
return Result.failure(Exception("Not Implemented"))
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
}
|