....
This commit is contained in:
@@ -25,6 +25,9 @@ import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import model.*
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class KisTradeService {
|
||||
private val client = HttpClient(CIO) {
|
||||
@@ -173,6 +176,66 @@ class KisTradeService {
|
||||
} catch (e: Exception) { Result.failure(e) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [추가] 기간별(일/주/월) 차트 데이터 조회
|
||||
* @param periodCode "D"(일), "W"(주), "M"(월)
|
||||
*/
|
||||
suspend fun fetchPeriodChartData(
|
||||
stockCode: String,
|
||||
periodCode: String = "D",
|
||||
isDomestic: Boolean = true
|
||||
): Result<List<CandleData>> {
|
||||
val config = KisSession.config
|
||||
val path = if (isDomestic) "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
|
||||
else "/uapi/overseas-stock/v1/quotations/inquire-daily-itemchartprice"
|
||||
|
||||
val today = LocalDate.now()
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyyMMdd")
|
||||
val endDate = today.format(formatter)
|
||||
|
||||
// [수정] 100개를 가져오기 위해 시작일을 너무 멀지 않게 설정 (약 6개월 전)
|
||||
// 이렇게 하면 종료일(오늘)부터 소급하여 최대 100개의 최신 데이터를 안전하게 가져옵니다.
|
||||
val startDate = when (periodCode) {
|
||||
"D" -> today.minusMonths(6).format(formatter) // 일봉: 6개월치면 100개 충분
|
||||
"W" -> today.minusYears(2).format(formatter) // 주봉: 2년치
|
||||
"M" -> today.minusYears(8).format(formatter) // 월봉: 8년치
|
||||
else -> today.minusYears(1).format(formatter)
|
||||
}
|
||||
return try {
|
||||
val response = client.get("$prodUrl$path") {
|
||||
header("authorization", "Bearer ${config.marketToken}")
|
||||
header("appkey", config.realAppKey)
|
||||
header("appsecret", config.realSecretKey)
|
||||
header("tr_id", if (isDomestic) "FHKST03010100" else "HHDFS76240000")
|
||||
header("custtype", "P")
|
||||
|
||||
parameter("FID_INPUT_DATE_1", startDate)
|
||||
parameter("FID_INPUT_DATE_2", endDate)
|
||||
parameter("FID_COND_MRKT_DIV_CODE", "J")
|
||||
parameter("FID_INPUT_ISCD", stockCode)
|
||||
parameter("FID_PERIOD_DIV_CODE", periodCode) // D, W, M
|
||||
parameter("FID_ORG_ADJ_PRC", "0")
|
||||
}
|
||||
|
||||
val body = response.body<JsonObject>()
|
||||
val output2 = body["output2"]?.jsonArray
|
||||
|
||||
val candles = output2?.map { element ->
|
||||
val obj = element.jsonObject
|
||||
CandleData(
|
||||
stck_bsop_date = obj["stck_bsop_date"]?.jsonPrimitive?.content ?: "",
|
||||
stck_clpr = obj["stck_clpr"]?.jsonPrimitive?.content ?: "0",
|
||||
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["acml_vol"]?.jsonPrimitive?.content ?: "0"
|
||||
)
|
||||
}?.reversed() ?: emptyList()
|
||||
|
||||
Result.success(candles)
|
||||
} catch (e: Exception) { Result.failure(e) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [해외 주식 순위] 모델 매핑 오류 수정
|
||||
*/
|
||||
@@ -217,22 +280,98 @@ class KisTradeService {
|
||||
suspend fun postOrder(
|
||||
stockCode: String,
|
||||
qty: String,
|
||||
price: String, // "0" 이면 시장가
|
||||
price: String,
|
||||
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
|
||||
|
||||
// 계좌번호 처리: 8자리면 01 자동 추가
|
||||
var pureAccount = config.accountNo.replace("-", "").trim()
|
||||
if (pureAccount.length == 8) pureAccount += "01"
|
||||
|
||||
val cano = pureAccount.take(8)
|
||||
val acntPrdtCd = pureAccount.takeLast(2)
|
||||
|
||||
val trId = when {
|
||||
isDomestic && config.isSimulation -> if (isBuy) "VTRP0001U" else "VTRP0002U"
|
||||
isDomestic && config.isSimulation -> if (isBuy) "VTTC0802U" else "VTTC0801U"
|
||||
isDomestic && !config.isSimulation -> if (isBuy) "TTTC0802U" else "TTTC0801U"
|
||||
!isDomestic && config.isSimulation -> if (isBuy) "VTTT3001U" else "VTTT3002U"
|
||||
else -> if (isBuy) "TTTS3001U" else "TTTS3002U"
|
||||
else -> if (isBuy) "TTTS3002U" else "TTTS3001U"
|
||||
}
|
||||
|
||||
return try {
|
||||
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("custtype", "P") // [해결] 필수 헤더 추가
|
||||
header("Content-Type", "application/json")
|
||||
|
||||
setBody(mapOf(
|
||||
"CANO" to cano,
|
||||
"ACNT_PRDT_CD" to acntPrdtCd,
|
||||
"PDNO" to stockCode,
|
||||
"ORD_DVSN" to if (price == "0" || price.isEmpty()) "01" else "00",
|
||||
"ORD_QTY" to qty,
|
||||
"ORD_UNPR" to if (price.isEmpty() || price == "0") "0" else price
|
||||
))
|
||||
}
|
||||
|
||||
val body = response.body<JsonObject>() // [해결] Polymorphic 직렬화 에러 방지
|
||||
val rtCd = body["rt_cd"]?.jsonPrimitive?.content
|
||||
val msg = body["msg1"]?.jsonPrimitive?.content ?: "메시지 없음"
|
||||
|
||||
if (rtCd == "0") Result.success("✅ 주문 성공: $msg")
|
||||
else Result.failure(Exception("❌ 오류 ($rtCd): $msg"))
|
||||
} catch (e: Exception) { Result.failure(e) }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [추가] 국내 미체결 내역 조회
|
||||
*/
|
||||
suspend fun fetchUnfilledOrders(): Result<List<UnfilledOrder>> {
|
||||
val config = KisSession.config
|
||||
if (config.isSimulation) Result.success(emptyList<UnfilledOrder>())
|
||||
val baseUrl = if (config.isSimulation) vtsUrl else prodUrl
|
||||
val trId = "TTTC0084R"
|
||||
|
||||
return try {
|
||||
val response = client.get("$baseUrl/uapi/domestic-stock/v1/trading/inquire-psbl-rvsecncl") {
|
||||
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("custtype", "P")
|
||||
|
||||
parameter("CANO", config.accountNo.take(8))
|
||||
parameter("ACNT_PRDT_CD", config.accountNo.takeLast(2))
|
||||
parameter("CTX_AREA_FK100", "")
|
||||
parameter("CTX_AREA_NK100", "")
|
||||
parameter("T_GUBUN", "0")
|
||||
parameter("LOAN_DT", "")
|
||||
parameter("P_S_GUBUN", "0")
|
||||
parameter("INQR_DVSN_1", "0")
|
||||
parameter("INQR_DVSN_2", "0")
|
||||
}
|
||||
val body = response.body<UnfilledResponse>()
|
||||
if (body.rt_cd == "0") Result.success(body.output)
|
||||
else Result.failure(Exception(body.msg1))
|
||||
} catch (e: Exception) { Result.failure(e) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [추가] 주문 취소 (정정/취소 API)
|
||||
*/
|
||||
suspend fun cancelOrder(orgNo: String, stockCode: String): Result<String> {
|
||||
val config = KisSession.config
|
||||
val baseUrl = if (config.isSimulation) vtsUrl else prodUrl
|
||||
val trId = if (config.isSimulation) "VTTC0803U" else "TTTC0803U"
|
||||
|
||||
return try {
|
||||
val response = client.post("$baseUrl/uapi/domestic-stock/v1/trading/order-rvsecncl") {
|
||||
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)
|
||||
@@ -242,15 +381,17 @@ class KisTradeService {
|
||||
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
|
||||
"KRX_FWDG_ORD_ORGNO" to "", // 공란 혹은 지점번호
|
||||
"ORGN_ORD_NO" to orgNo, // 취소할 원주문번호
|
||||
"RVSE_CNCL_DVSN" to "02", // 01: 정정, 02: 취소
|
||||
"ORD_DVSN" to "00", // 지정가
|
||||
"ORD_QTY" to "0", // 0이면 전량 취소
|
||||
"ORD_UNPR" to "0"
|
||||
))
|
||||
}
|
||||
val body = response.body<Map<String, Any>>()
|
||||
if (body["rt_cd"] == "0") Result.success("✅ 주문 성공: ${body["msg1"]}")
|
||||
else Result.failure(Exception("${body["msg1"]}"))
|
||||
val body = response.body<JsonObject>()
|
||||
if (body["rt_cd"]?.jsonPrimitive?.content == "0") Result.success("취소 완료")
|
||||
else Result.failure(Exception(body["msg1"]?.jsonPrimitive?.content))
|
||||
} catch (e: Exception) { Result.failure(e) }
|
||||
}
|
||||
|
||||
@@ -284,7 +425,12 @@ class KisTradeService {
|
||||
val path = if (isDomestic)
|
||||
"/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice"
|
||||
else "/uapi/overseas-stock/v1/quotations/inquire-time-itemchartprice"
|
||||
|
||||
val now = LocalTime.now()
|
||||
val searchTime = if (now.isAfter(LocalTime.of(15, 30))) {
|
||||
"153000"
|
||||
} else {
|
||||
now.format(DateTimeFormatter.ofPattern("HHmmss"))
|
||||
}
|
||||
return try {
|
||||
val response = client.get("$prodUrl$path") {
|
||||
header("authorization", "Bearer ${config.marketToken}")
|
||||
@@ -297,7 +443,7 @@ class KisTradeService {
|
||||
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_INPUT_HOUR_1", searchTime) // 장 마감 시간까지
|
||||
parameter("FID_PW_DATA_INCU_YN", "Y") // 전일 데이터 포함 여부
|
||||
}
|
||||
|
||||
|
||||
@@ -6,61 +6,49 @@ import androidx.compose.ui.graphics.Color
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
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.plugins.websocket.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import model.AppConfig
|
||||
import model.KisSession
|
||||
import model.RealTimeTrade
|
||||
import model.TradeType
|
||||
|
||||
class KisWebSocketManager {
|
||||
private val client = HttpClient(CIO) {
|
||||
install(WebSockets) {
|
||||
pingInterval = 20_000
|
||||
}
|
||||
install(WebSockets) { pingInterval = 20_000 }
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 15_000
|
||||
connectTimeoutMillis = 15_000
|
||||
socketTimeoutMillis = 15_000
|
||||
}
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.ALL // 상세한 디버깅을 위해 ALL로 변경
|
||||
}
|
||||
}
|
||||
|
||||
private var session: DefaultClientWebSocketSession? = null
|
||||
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||
|
||||
// UI 관찰 상태값
|
||||
// UI 상태값
|
||||
val currentPrice = mutableStateOf("0")
|
||||
val priceChangeColor = mutableStateOf(Color.Transparent)
|
||||
val tradeLogs = mutableStateListOf<RealTimeTrade>()
|
||||
|
||||
// 콜백: 체결 발생 시 (주문번호, 종목코드, 가격, 수량, 매수/매도여부)
|
||||
var onExecutionReceived: ((orderNo: String, code: String, price: String, qty: String, isBuy: Boolean) -> Unit)? = null
|
||||
|
||||
// 콜백: 감시 조건 도달 시 (종목코드, 현재가, 타입)
|
||||
var onTargetReached: ((code: String, price: Double, isProfit: Boolean) -> Unit)? = null
|
||||
|
||||
suspend fun connect() {
|
||||
val config = KisSession.config
|
||||
val approvalKey = config.websocketToken
|
||||
if (config.websocketToken.isEmpty()) return
|
||||
|
||||
if (approvalKey.isEmpty()) {
|
||||
println("⚠️ 웹소켓 승인키가 없습니다. 먼저 발급받아야 합니다.")
|
||||
return
|
||||
}
|
||||
|
||||
// 시세 데이터는 항상 실전 서버(21000)를 권장합니다.
|
||||
val hostUrl = "ops.koreainvestment.com"
|
||||
val port = 21000
|
||||
val port = 21000 // 실전: 21000, 모의: 21000 (동일하나 TR_ID 등에 따라 다름)
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
client.webSocket(method = HttpMethod.Get, host = hostUrl, port = port, path = "/tryitout/H0STCNT0") {
|
||||
session = this
|
||||
println("✅ 웹소켓 연결 성공")
|
||||
println("✅ 웹소켓 서버 연결 성공")
|
||||
|
||||
incoming.consumeAsFlow().collect { frame ->
|
||||
if (frame is Frame.Text) {
|
||||
@@ -75,66 +63,107 @@ class KisWebSocketManager {
|
||||
}
|
||||
|
||||
private fun parseTradeData(data: String) {
|
||||
// 한국투자증권 데이터 포맷: 수신구분|TRID|데이터건수|체결데이터
|
||||
// KIS 데이터 포맷: 수신구분|TRID|데이터건수|체결데이터
|
||||
val parts = data.split("|")
|
||||
if (parts.size > 3) {
|
||||
val rows = parts[3].split("^")
|
||||
if (rows.size > 15) {
|
||||
val newTrade = RealTimeTrade(
|
||||
time = rows[1].chunked(2).joinToString(":"), // HHMMSS -> HH:MM:SS
|
||||
price = rows[2],
|
||||
change = rows[4],
|
||||
volume = rows[12],
|
||||
type = if (rows[15] == "1") TradeType.BUY else TradeType.SELL
|
||||
)
|
||||
if (parts.size < 4) return
|
||||
|
||||
// 메인 스레드에서 UI 상태 업데이트
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
tradeLogs.add(0, newTrade) // 최신 데이터를 맨 위로
|
||||
if (tradeLogs.size > 30) tradeLogs.removeLast()
|
||||
val trId = parts[1]
|
||||
val body = parts[3]
|
||||
|
||||
// 현재가 및 색상 업데이트 로직 포함 가능
|
||||
currentPrice.value = newTrade.price
|
||||
}
|
||||
when (trId) {
|
||||
"H0STCNT0" -> handlePriceData(body) // [1] 실시간 시세 처리
|
||||
"H0STCNI0" -> handleExecutionData(body) // [2] 실시간 체결 통보 처리
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [1] 실시간 가격 데이터 처리 및 감시 로직
|
||||
*/
|
||||
private fun handlePriceData(body: String) {
|
||||
val rows = body.split("^")
|
||||
if (rows.size < 16) return
|
||||
|
||||
val stockCode = rows[0]
|
||||
val priceStr = rows[2]
|
||||
val currentPriceInt = priceStr.toIntOrNull() ?: 0
|
||||
|
||||
val newTrade = RealTimeTrade(
|
||||
time = rows[1].chunked(2).joinToString(":"),
|
||||
price = priceStr,
|
||||
change = rows[4],
|
||||
volume = rows[12],
|
||||
type = if (rows[15] == "1") TradeType.BUY else TradeType.SELL
|
||||
)
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
tradeLogs.add(0, newTrade)
|
||||
if (tradeLogs.size > 30) tradeLogs.removeLast()
|
||||
currentPrice.value = String.format("%,d", currentPriceInt)
|
||||
|
||||
// 실시간 감시 엔진 작동
|
||||
checkAutoTradeTargets(stockCode, currentPriceInt.toDouble())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [2] 실시간 개인 체결 통보 처리
|
||||
*/
|
||||
private fun handleExecutionData(body: String) {
|
||||
val rows = body.split("^")
|
||||
if (rows.size < 13) return
|
||||
|
||||
val orderNo = rows[1]
|
||||
val stockCode = rows[7]
|
||||
val side = rows[9] // 01: 매도, 02: 매수
|
||||
val price = rows[11]
|
||||
val qty = rows[12]
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val isBuy = side == "02"
|
||||
println("📣 체결 통보 수신: $stockCode | ${if(isBuy) "매수" else "매도"} | $price 원")
|
||||
|
||||
// 외부 콜백 실행 (DB 업데이트 및 UI 전환 트리거)
|
||||
onExecutionReceived?.invoke(orderNo, stockCode, price, qty, isBuy)
|
||||
|
||||
// 매수 체결 시 즉시 해당 종목 실시간 시세 구독 시작
|
||||
if (isBuy) subscribeStock(stockCode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동매매 목표가 도달 여부 판단
|
||||
*/
|
||||
private fun checkAutoTradeTargets(code: String, currentPrice: Double) {
|
||||
// DB에서 해당 종목의 감시 설정(익절/손절가)을 가져와 비교
|
||||
// 효율성을 위해 Map 등에 캐싱하여 사용할 것을 권장
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val config = DatabaseFactory.findConfigByCode(code) ?: return@launch
|
||||
|
||||
if (currentPrice >= config.targetPrice) {
|
||||
withContext(Dispatchers.Main) { onTargetReached?.invoke(code, currentPrice, true) }
|
||||
} else if (currentPrice <= config.stopLossPrice) {
|
||||
withContext(Dispatchers.Main) { onTargetReached?.invoke(code, currentPrice, false) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private fun updatePriceWithEffect(newPrice: String) {
|
||||
val oldPrice = currentPrice.value.replace(",", "").toIntOrNull() ?: 0
|
||||
val current = newPrice.toIntOrNull() ?: 0
|
||||
|
||||
currentPrice.value = String.format("%, d", current)
|
||||
priceChangeColor.value = when {
|
||||
current > oldPrice -> Color.Red.copy(alpha = 0.2f)
|
||||
current < oldPrice -> Color.Blue.copy(alpha = 0.2f)
|
||||
else -> Color.Transparent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [2] 실시간 시세 구독 (Registration)
|
||||
* tr_type = "1" (등록)
|
||||
* 개인 체결 통보 구독 (HTS ID 필요)
|
||||
*/
|
||||
suspend fun subscribeExecution(htsId: String) {
|
||||
sendRequest(htsId, trType = "1", trId = "H0STCNI0")
|
||||
println("📡 실시간 체결 통보 구독 시작: $htsId")
|
||||
}
|
||||
|
||||
suspend fun subscribeStock(stockCode: String) {
|
||||
sendRequest(stockCode, trType = "1")
|
||||
println("📡 실시간 시세 구독 시작: $stockCode")
|
||||
sendRequest(stockCode, trType = "1", trId = "H0STCNT0")
|
||||
}
|
||||
|
||||
/**
|
||||
* [3] 실시간 시세 구독 취소 (Unsubscription)
|
||||
* tr_type = "2" (해제)
|
||||
*/
|
||||
suspend fun unsubscribeStock(stockCode: String) {
|
||||
if (stockCode.isEmpty()) return
|
||||
sendRequest(stockCode, trType = "2")
|
||||
println("🚫 실시간 시세 구독 해제: $stockCode")
|
||||
if (stockCode.isNotEmpty()) sendRequest(stockCode, trType = "2", trId = "H0STCNT0")
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 요청 전송 함수
|
||||
*/
|
||||
private suspend fun sendRequest(stockCode: String, trType: String) {
|
||||
private suspend fun sendRequest(key: String, trType: String, trId: String) {
|
||||
val currentSession = session ?: return
|
||||
val config = KisSession.config
|
||||
|
||||
@@ -148,8 +177,8 @@ class KisWebSocketManager {
|
||||
},
|
||||
"body": {
|
||||
"input": {
|
||||
"tr_id": "H0STCNT0",
|
||||
"tr_key": "$stockCode"
|
||||
"tr_id": "$trId",
|
||||
"tr_key": "$key"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +187,12 @@ class KisWebSocketManager {
|
||||
try {
|
||||
currentSession.send(Frame.Text(requestJson))
|
||||
} catch (e: Exception) {
|
||||
println("❌ 웹소켓 요청 실패 ($trType): ${e.localizedMessage}")
|
||||
println("❌ 웹소켓 요청 실패 ($trId): ${e.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
fun clearData() {
|
||||
tradeLogs.clear()
|
||||
currentPrice.value = "0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user