Skip to content

Commit

Permalink
Merge pull request #35 from f-lab-edu/feature/ct-2-2-3-chart-game-bug…
Browse files Browse the repository at this point in the history
…-fix

�[CT-2-2-3] 차트게임 기능 - 실행 테스트 및 버그 수정
  • Loading branch information
f-lab-nathan authored Jun 11, 2024
2 parents b952f0a + d610fd1 commit 5298aeb
Show file tree
Hide file tree
Showing 26 changed files with 446 additions and 181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.yessorae.data.source.network.polygon.model.chart.asDomainModel
import com.yessorae.domain.common.ChartRequestArgumentHelper
import com.yessorae.domain.entity.Chart
import com.yessorae.domain.entity.tick.TickUnit
import com.yessorae.domain.exception.ChartGameException
import com.yessorae.domain.repository.ChartRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
Expand All @@ -23,19 +24,48 @@ class ChartRepositoryImpl @Inject constructor(
@Dispatcher(ChartTrainerDispatcher.IO)
private val dispatcher: CoroutineDispatcher
) : ChartRepository {
override suspend fun fetchNewChartRandomly(): Chart =
override suspend fun fetchNewChartRandomly(totalTurn: Int): Chart =
withContext(dispatcher) {
val chart = networkDataSource
.getChart(
ticker = chartRequestArgumentHelper.getRandomTicker(),
tickUnit = appPreferences.getTickUnit(),
from = chartRequestArgumentHelper.getFromDate(),
to = chartRequestArgumentHelper.getToDate()
)
.asDomainModel(TickUnit.DAY)

val chartId = localDBDataSource.insertChart(chart.asEntity())
localDBDataSource.insertTicks(chart.ticks.map { it.asEntity(chartId = chartId) })
chart.copy(id = chartId)
fetchNewChartRandomlyWithRetry(
currentRetryCount = 0,
totalTurn = totalTurn
)
}

private suspend fun fetchNewChartRandomlyWithRetry(
currentRetryCount: Int,
totalTurn: Int
): Chart {
// RETRY_COUNT 만큼 시도했는데도 실패하면 IllegalStateException 발생.
// 거의 발생하지 않아도 안전망 역할
if (currentRetryCount > RETRY_COUNT) {
throw ChartGameException.HardToFetchTradeException
}

val dto = networkDataSource
.getChart(
ticker = chartRequestArgumentHelper.getRandomTicker(),
tickUnit = appPreferences.getTickUnit(),
from = chartRequestArgumentHelper.getFromDate(),
to = chartRequestArgumentHelper.getToDate()
)

// 서버에서 가져온 차트가 totalTurn 보다 작으면 다시 요청
if (dto.ticks.size < totalTurn) {
return fetchNewChartRandomlyWithRetry(
currentRetryCount = currentRetryCount + 1,
totalTurn = totalTurn
)
}

val chart = dto.asDomainModel(TickUnit.DAY)

val chartId = localDBDataSource.insertChart(chart.asEntity())
localDBDataSource.insertTicks(chart.ticks.map { it.asEntity(chartId = chartId) })
return chart.copy(id = chartId)
}

companion object {
private const val RETRY_COUNT = 3
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ data class ChartDto(
@SerializedName("request_id")
val requestId: String,
@SerializedName("results")
val ticks: List<TickDto>?,
val ticks: List<TickDto> = listOf(),
@SerializedName("resultsCount")
val ticksCount: Int,
val status: String
Expand All @@ -22,9 +22,9 @@ data class ChartDto(
fun ChartDto.asDomainModel(tickUnit: TickUnit): Chart {
return Chart(
tickerSymbol = ticker,
startDateTime = ticks?.firstOrNull()?.startTimestamp?.toLocalDateTime(),
endDateTime = ticks?.lastOrNull()?.startTimestamp?.toLocalDateTime(),
ticks = ticks?.map(TickDto::asDomainModel) ?: listOf(),
startDateTime = ticks.firstOrNull()?.startTimestamp?.toLocalDateTime(),
endDateTime = ticks.lastOrNull()?.startTimestamp?.toLocalDateTime(),
ticks = ticks.map(TickDto::asDomainModel) ?: listOf(),
tickUnit = tickUnit
)
}
4 changes: 2 additions & 2 deletions domain/src/main/java/com/yessorae/domain/common/Result.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.yessorae.domain.common
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart

sealed class Result<out T> {
Expand Down Expand Up @@ -31,7 +31,7 @@ private fun <T> Flow<T>.mapToSuccessResult(): Flow<Result<T>> {
}

private fun Flow<Result<Unit>>.emitSuccessResultOnEmpty(): Flow<Result<Unit>> {
return this.onEmpty {
return this.onCompletion {
emit(Result.Success(data = Unit))
}
}
Expand Down
61 changes: 46 additions & 15 deletions domain/src/main/java/com/yessorae/domain/entity/ChartGame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,60 @@ data class ChartGame(
// 유저의 게임 강제 종료 여부
val isQuit: Boolean
) {
val totalProfit: Money = currentBalance - startBalance

val rateOfProfit: Double = (totalProfit / startBalance).value * 100
private val sortedTicks = chart.ticks.sortedBy { it.startTimestamp }

val tradeCount: Int
get() = trades.size
private val lastVisibleIndex = (chart.ticks.size - 1) - (totalTurn - currentTurn)

val totalCommission: Money = Money(trades.sumOf { trade -> trade.commission.value })
// 현재 턴의까지의 차트 데이터
val visibleTicks: List<Tick> = if (chart.ticks.size <= lastVisibleIndex) {
sortedTicks
} else {
sortedTicks.subList(0, lastVisibleIndex)
}

// 보유 주식 수량
val ownedStockCount = trades.sumOf { trade ->
if (trade.type.isBuy()) {
trade.count
} else {
-trade.count
}
}

val visibleTicks: List<Tick> =
chart.ticks
.sortedBy { it.startTimestamp }
.subList(0, chart.ticks.size - totalTurn + currentTurn - 1)
// 보유 주식 총 가치
private val ownedTotalStockPrice = trades.sumOf { trade ->
if (trade.type.isBuy()) {
trade.totalTradeMoney.value
} else {
-trade.totalTradeMoney.value
}
}

val ownedStockCount = trades.sumOf { trade -> trade.count }
// 현재 보유 주식 평단가
val ownedAverageStockPrice = if (ownedStockCount != 0) {
Money(ownedTotalStockPrice / ownedStockCount)
} else {
Money(0.0)
}

private val ownedTotalStockPrice = trades.sumOf { trade -> trade.totalTradeMoney.value }
// 현재 종가
val currentClosePrice: Money = (visibleTicks.lastOrNull()?.closePrice ?: Money(0.0))

val ownedAverageStockPrice = Money(ownedTotalStockPrice / ownedStockCount)
// 누적 수익
val accumulatedTotalProfit: Money = trades.fold(Money(0.0)) { acc, trade ->
acc + trade.profit
} + if (ownedStockCount != 0) {
currentClosePrice - ownedAverageStockPrice
} else {
Money(0.0)
}

val currentStockPrice: Money = visibleTicks.lastOrNull()?.closePrice ?: Money(0.0)
// 누적 수익률
val accumulatedRateOfProfit: Double = (accumulatedTotalProfit / startBalance).value

val currentGameProgress: Float = currentTurn / totalTurn.toFloat() * 100f
// 현재 게임 진행률
val currentGameProgress: Float = currentTurn / totalTurn.toFloat()

// 게임 모든 턴을 끝까지 완료한 경우 true
val isGameComplete: Boolean = currentTurn == totalTurn
Expand All @@ -70,7 +101,7 @@ data class ChartGame(
internal fun copyFrom(newTrade: Trade): ChartGame {
return copy(
trades = trades + newTrade,
currentBalance = currentBalance + newTrade.profit
currentBalance = currentBalance - newTrade.totalTradeMoney
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ data class Trade(
// 수수료
val commission: Money = totalTradeMoney * commissionRate

// 실현 손익
val profit: Money = ((stockPrice - ownedAverageStockPrice) * count) - commission
// 실현 손익, 매도할 때만 유효
val profit: Money = if (type.isBuy()) {
Money(0.0)
} else {
((stockPrice - ownedAverageStockPrice) * count) - commission
}

companion object {
internal fun new(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ package com.yessorae.domain.entity.trade

enum class TradeType {
Buy,
Sell
Sell;

fun isBuy(): Boolean = this == Buy
fun isSell(): Boolean = this == Sell
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ sealed class ChartGameException(override val message: String = "") : Exception(m
override val message: String
) : ChartGameException(message = message)

// 설정한 제한 보다 많은 재시도를 했음에도 조건에 맞는 차트를 찾지 못했을 때 발생하는 예외
object HardToFetchTradeException : ChartGameException("")

data class CanNotChangeTradeException(
override val message: String
) : ChartGameException(message = message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package com.yessorae.domain.repository
import com.yessorae.domain.entity.Chart

interface ChartRepository {
suspend fun fetchNewChartRandomly(): Chart
suspend fun fetchNewChartRandomly(totalTurn: Int): Chart
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import com.yessorae.domain.common.delegateEmptyResultFlow
import com.yessorae.domain.exception.ChartGameException
import com.yessorae.domain.repository.ChartGameRepository
import com.yessorae.domain.repository.ChartRepository
import com.yessorae.domain.repository.UserRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class ChangeChartUseCase @Inject constructor(
private val userRepository: UserRepository,
private val chartRepository: ChartRepository,
private val chartGameRepository: ChartGameRepository
) {
Expand All @@ -25,7 +27,9 @@ class ChangeChartUseCase @Inject constructor(

chartGameRepository.updateChartGame(
chartGame = oldChartGame.copyFrom(
newChart = chartRepository.fetchNewChartRandomly()
newChart = chartRepository.fetchNewChartRandomly(
totalTurn = userRepository.fetchTotalTurnConfig()
)
)
)
}.delegateEmptyResultFlow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ class SubscribeChartGameUseCase @Inject constructor(
) {
suspend operator fun invoke(gameId: Long?): Flow<Result<ChartGame>> {
if (gameId == null) {
val totalTurn = userRepository.fetchTotalTurnConfig()

val newGameId = chartGameRepository.createNewChartGame(
chartGame = ChartGame.new(
chart = chartRepository.fetchNewChartRandomly(),
totalTurn = userRepository.fetchTotalTurnConfig(),
chart = chartRepository.fetchNewChartRandomly(totalTurn = totalTurn),
totalTurn = totalTurn,
startBalance = userRepository.fetchCurrentBalance()
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ class UpdateNextTickUseCase @Inject constructor(
) {
operator fun invoke(gameId: Long): Flow<Result<Unit>> =
flow<Nothing> {
val newChartGame = chartGameRepository.fetchChartGame(gameId = gameId).getNextTurn()
val oldChartGame = chartGameRepository.fetchChartGame(gameId = gameId)

if (newChartGame.isGameEnd) {
if (oldChartGame.isGameEnd) {
throw ChartGameException.CanNotUpdateNextTickException(
message = "can't update next tick because game has been end"
)
}

chartGameRepository.updateChartGame(chartGame = newChartGame)
chartGameRepository.updateChartGame(chartGame = oldChartGame.getNextTurn())
}.delegateEmptyResultFlow()
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.yessorae.presentation.ui.chartgame.ChartGameScreen
import com.yessorae.presentation.ui.designsystem.theme.ChartTrainerTheme
import dagger.hilt.android.AndroidEntryPoint

Expand All @@ -24,7 +25,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
ChartGameScreen()
}
}
}
Expand Down
Loading

0 comments on commit 5298aeb

Please sign in to comment.