Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CT-2-2-3] 차트게임 기능 - 실행 테스트 및 버그 수정 #35

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
85cada9
[CT-2-2-3] design: 상태바 아이콘 컬러 반전
nosorae Jun 3, 2024
3e0e04c
[CT-2-2-3] design: 주문 UI 얼라인 바텀 처리
nosorae Jun 3, 2024
0573512
[CT-2-2-3] design: 매매 주문 Ui ShortCurChip 패딩/간격 수정
nosorae Jun 3, 2024
012f3cd
[CT-2-2-3] fix: NumberFormatException 으로 발생하는 크래시 수정
nosorae Jun 3, 2024
c9f5535
[CT-2-2-3] design: TradeOrderUi 키패드 애니메이션 제거
nosorae Jun 3, 2024
c0c9c88
[CT-2-2-3] design: TradeOrderUi 모달바텀시트로 변경 (뒷배경, 클릭 방지, 애니메이션 등의 이유)
nosorae Jun 3, 2024
a922675
[CT-2-2-3] style: 불필요 필드 제거
nosorae Jun 3, 2024
568e9ac
[CT-2-2-3] fix: TradeOrderUi KeyPad 버그 수정, userInput non-null로 변경
nosorae Jun 3, 2024
d3cadd8
[CT-2-2-3] design: TradeOrderUi 금액 소수점 두 자리수로 자르기 (함수 하나로 통일하게 리팩토링 예정)
nosorae Jun 3, 2024
d2f060d
[CT-2-2-3] design: TradeOrderUi 하단 버튼 shape 수정
nosorae Jun 3, 2024
17809f7
[CT-2-2-3] design: 로딩 프로그래스 바 추가
nosorae Jun 4, 2024
068198d
[CT-2-2-3] fix: currentGameProgress 를 % 정수가 아니라 소수로 표현되게 수정
nosorae Jun 4, 2024
d3ccf97
[CT-2-2-3] fix: TopAppBar 초기 상태는 타이틀로 보여주기
nosorae Jun 4, 2024
afd02be
[CT-2-2-3] design: ChartGameBottomBarUi 에 디바이더 추가, 간격조정
nosorae Jun 4, 2024
c949dc7
[CT-2-2-3] fix: ChartGameBottomBarUi 매도매수 버튼 활성화 조건 수정
nosorae Jun 4, 2024
c123aab
[CT-2-2-3] fix: 다음 턴(틱) 보여주는 기능 정상화
nosorae Jun 4, 2024
af50e7c
[CT-2-2-3] fix: NaN 처리 추가
nosorae Jun 4, 2024
c6d1c4d
[CT-2-2-3] fix: Flow<Result<Unit>> 일 때 Success 값 오지 않는 이슈 수정
nosorae Jun 4, 2024
8568076
[CT-2-2-3] fix: 매매기능 정상화, 매도 주문 커스텀 소프트 인풋에 대해 toInt 누락 추가 처리
nosorae Jun 6, 2024
95391ad
[CT-2-2-3] fix: 불러온 차트가 틱이 50개 미만이면 게임을 정상적으로 진행할 수 없으니 재시도와 가이드 추가
nosorae Jun 6, 2024
2c69b2a
[CT-2-2-3] style: ktlintFormat
nosorae Jun 6, 2024
5fa51fd
[CT-2-2-3] fix: 게임 종료 정상화
nosorae Jun 6, 2024
4310b54
[CT-2-2-3] fix: 버튼 활성화 여부 조건에 게임종료인 경우 고려
nosorae Jun 6, 2024
c47db0b
Merge branch 'feature/ct-2-2-2-chart-game-presentation-screen' into f…
nosorae Jun 6, 2024
9019b5a
[CT-2-2-3] refactor: Screen 으로부터 이벤트 분기를 분리
nosorae Jun 6, 2024
7ee037b
[2-2-3] fix: 전량 매도시에 totalProfit, rateOfProfit 이 0이 되는 버그 수정 (totalPr…
nosorae Jun 9, 2024
59d2c0f
[CT-2-2-3] fix: 음수일 때도 버튼 활성화되게 수정
nosorae Jun 9, 2024
c1231e7
[CT-2-2-3] 리뷰반영 - dto의 ticks를 널 대신 빈 리스트로 초기화
nosorae Jun 10, 2024
8a07b8e
[CT-2-2-3] 리뷰반영 - 차트의 tick 리스트가 부족할 때 그대로 보여준다.
nosorae Jun 10, 2024
3b02a4f
[CT-2-2-3] 리뷰반영 - 불필요한 소괄호 제거
nosorae Jun 10, 2024
d610fd1
[CT-2-2-3] 리뷰반영 - 코드의 간결함을 위해 반복되는 코드는 확장함수등으로 만들기
nosorae Jun 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설마 S&P500에 1년미만인 종목이 있을까 하며 처리 안하고 넘어갔던 부분인데, TotalTurn 보다 틱수가 낮으면 게임이 불가하여 TotalTurn 보다는 큰 게 나올 때까지 시도하되 무한 루프를 돌면 안되니 최대 횟수를 두었습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q; 이 역할을 가진 함수를 분리한 이유가 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchNewChartRandomly 함수를 사용하는 입장에서 알 필요 없는 currentRetryCount 같은 파라미터를 노출하지 않고 withContext 로 디스패칭을 바꿔주는 것을 한 번만 할 수 있어서 분리했습니다.

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
Loading