package network import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.parameter import io.ktor.client.request.post import io.ktor.client.request.setBody import jdk.javadoc.internal.doclets.formats.html.markup.HtmlStyle import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import model.KeyMetrics enum class OverseasRankType(val url: String, val trId: String) { PRICE_FLUCT("/uapi/overseas-stock/v1/ranking/price-fluct", "HHDFS76260000"), // 가격급등락 VOLUME_SURGE("/uapi/overseas-stock/v1/ranking/volume-surge", "HHDFS76270000"), // 거래량급증 VOLUME_POWER("/uapi/overseas-stock/v1/ranking/volume-power", "HHDFS76280000"), // 매수체결강도상위 UPDOWN_RATE("/uapi/overseas-stock/v1/ranking/updown-rate", "HHDFS76290000") // 상승률/하락률 } // --- [공통 응답 구조] --- @Serializable data class OverseasResponse( val rt_cd: String, // 0: 성공, 이외 실패 val msg_cd: String, val msg1: String, val output: T? = null, val output1: OverseasPriceOutput1? = null, // 시세 조회용 val output2: List = emptyList() // 시세 리스트용 ) // --- [랭킹/분석용 상세 아이템] --- @Serializable data class OverseasAnalysisItem( val excd: String = "", // 거래소 코드 val syml: String = "", // 종목 심볼 val name: String = "", // 종목명 val last: String = "", // 현재가 val rate: String = "", // 등락률 val tvol: String = "", // 거래량 val tamt: String = "" // 거래대금 ) // --- [시세 조회용 상세] --- @Serializable data class OverseasPriceOutput1(val rsym: String, val nrec: String) @Serializable data class OverseasPriceOutput2( val xymd: String, // 일자 val clos: String, // 종가 val open: String, // 시가 val high: String, // 고가 val low: String, // 저가 val tvol: String, // 거래량 val rate: String // 등락률 ) // --- [주문 응답] --- @Serializable data class OverseasOrderOutput( val ODNO: String, // 주문번호 val ORD_TMD: String // 주문시각 ) class KisOverseasService( private val client: HttpClient, private val appKey: String, private val appSecret: String, private var accessToken: String, private val accountNo: String, private val accountProdCode: String ) { private val baseUrl = "https://openapi.koreainvestment.com:9443" /** 1. 해외 랭킹 조회 (가격급등락, 거래량급증 등) */ suspend fun fetchRank(type: OverseasRankType, exchange: String): Result> { return try { val response = client.get("$baseUrl${type.url}") { header("authorization", "Bearer $accessToken") header("appkey", appKey) header("appsecret", appSecret) header("tr_id", type.trId) header("custtype", "P") parameter("AUTH", "") parameter("EXCD", exchange) if (type == OverseasRankType.PRICE_FLUCT) parameter("GUBN", "0") // 0:급등 } val body = response.body>>() if (body.rt_cd == "0") Result.success(body.output ?: emptyList()) else Result.failure(Exception(body.msg1)) } catch (e: Exception) { Result.failure(e) } } /** 2. 해외 주식 주문 (매수/매도) */ suspend fun sendOrder( isBuy: Boolean, exchange: String, code: String, qty: String, price: String ): Result { return try { val trId = if (isBuy) "TTTT1002U" else "TTTT1006U" val response = client.post("$baseUrl/uapi/overseas-stock/v1/trading/order") { header("authorization", "Bearer $accessToken") header("appkey", appKey) header("appsecret", appSecret) header("tr_id", trId) header("Content-Type", "application/json") setBody(mapOf( "CANO" to accountNo, "ACNT_PRDT_CD" to accountProdCode, "OVRS_EXCG_CD" to exchange, "PDNO" to code, "ORD_QTY" to qty, "OVRS_ORD_UNPR" to price, "ORD_SVR_DVSN_CD" to "00", "ORD_DVSN" to "00" )) } val body = response.body>() if (body.rt_cd == "0") Result.success(body.output!!) else Result.failure(Exception(body.msg1)) } catch (e: Exception) { Result.failure(e) } } /** 3. 전략적 시세 조회 (일/주/월 선택) */ suspend fun fetchChart( exchange: String, code: String, gubn: String // 0:일, 1:주, 2:월 ): List { return try { val response = client.get("$baseUrl/uapi/overseas-price/v1/quotations/dailyprice") { header("authorization", "Bearer $accessToken") header("appkey", appKey) header("appsecret", appSecret) header("tr_id", "HHDFS76240000") parameter("AUTH", "") parameter("EXCD", exchange) parameter("SYMB", code) parameter("GUBN", gubn) parameter("BYMD", "") parameter("MODP", "1") } val body = response.body>() if (body.rt_cd == "0") body.output2 else emptyList() } catch (e: Exception) { emptyList() } } } class OverseasAnalyzer( private val service: KisOverseasService, private val financialService: OverseasFinancialService ) { suspend fun executeAutoTrade() { // 1. 나스닥/뉴욕 거래량 급증 종목 스캔 val candidates = mutableListOf() listOf("NAS", "NYS").forEach { ex -> service.fetchRank(OverseasRankType.VOLUME_SURGE, ex).onSuccess { candidates.addAll(it) } } for (stock in candidates.distinctBy { it.syml }.take(10)) { // 2. 재무 필터링 (FMP API) - 부채비율 1.5 이하만 val metrics = financialService.fetchKeyMetrics(stock.syml).getOrNull() ?: continue if (metrics.debtToEquity > 1.5) continue // 3. 다중 차트 분석 (월봉으로 장기 추세 확인) val monthlyChart = service.fetchChart(stock.excd, stock.syml, "2") if (monthlyChart.size < 6) continue val isUpTrend = monthlyChart.first().clos.toDouble() > monthlyChart[5].clos.toDouble() // 4. 최종 매수 결정 if (isUpTrend) { // TradingLogStore.addLog("[해외매수결정] ${stock.name}(${stock.syml}) - 재무/추세 통과") service.sendOrder(true, stock.excd, stock.syml, "1", "0") // 시장가 전량 1주 예시 } } } } class OverseasFinancialService( private val client: HttpClient, private val apiKey: String // FMP에서 발급받은 키 ) { private val baseUrl = "https://financialmodelingprep.com/api/v3" /** * 특정 종목의 핵심 재무 지표를 가져옵니다. */ suspend fun fetchKeyMetrics(symbol: String): Result { return try { val response: List = client.get("$baseUrl/key-metrics-ttm/$symbol") { parameter("apikey", "FzvO_2P679KGRDUVx3u7rkMvCvcqiynu") }.body() if (response.isNotEmpty()) { Result.success(response[0]) } else { Result.failure(Exception("데이터가 없습니다.")) } } catch (e: Exception) { Result.failure(e) } } }