diff --git a/data/src/main/java/com/yessorae/data/source/local/database/converter/MoneyConverter.kt b/data/src/main/java/com/yessorae/data/source/local/database/converter/MoneyConverter.kt index 3da5943..f9932f2 100644 --- a/data/src/main/java/com/yessorae/data/source/local/database/converter/MoneyConverter.kt +++ b/data/src/main/java/com/yessorae/data/source/local/database/converter/MoneyConverter.kt @@ -2,10 +2,11 @@ package com.yessorae.data.source.local.database.converter import androidx.room.TypeConverter import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney class MoneyConverter { @TypeConverter - fun valueToMoney(value: Double?): Money? = value?.let { Money(it) } + fun valueToMoney(value: Double?): Money? = value?.let { it.asMoney() } @TypeConverter fun moneyToValue(money: Money?): Double? = money?.value diff --git a/data/src/main/java/com/yessorae/data/source/local/database/model/ChartGameEntity.kt b/data/src/main/java/com/yessorae/data/source/local/database/model/ChartGameEntity.kt index cf27a1e..44a1b84 100644 --- a/data/src/main/java/com/yessorae/data/source/local/database/model/ChartGameEntity.kt +++ b/data/src/main/java/com/yessorae/data/source/local/database/model/ChartGameEntity.kt @@ -76,7 +76,6 @@ fun ChartGameEntity.asDomainModel() = isQuit = isQuit, closeStockPrice = closeStockPrice, totalStockCount = totalStockCount, - totalStockPrice = totalStockPrice, averageStockPrice = averageStockPrice, accumulatedTotalProfit = accumulatedTotalProfit ) diff --git a/data/src/main/java/com/yessorae/data/source/local/database/model/TradeEntity.kt b/data/src/main/java/com/yessorae/data/source/local/database/model/TradeEntity.kt index 7b00385..503fddc 100644 --- a/data/src/main/java/com/yessorae/data/source/local/database/model/TradeEntity.kt +++ b/data/src/main/java/com/yessorae/data/source/local/database/model/TradeEntity.kt @@ -13,6 +13,8 @@ data class TradeEntity( val id: Long = 0, @ColumnInfo(name = COL_GAME_ID) val gameId: Long, + @ColumnInfo(name = COL_OWNED_STOCK_COUNT) + val ownedStockCount: Int, @ColumnInfo(name = COL_OWNED_AVERAGE_STOCK_PRICE) val ownedAverageStockPrice: Money, @ColumnInfo(name = COL_STOCK_PRICE) @@ -29,6 +31,7 @@ data class TradeEntity( companion object { const val NAME = "trade_table" const val COL_GAME_ID = "game_id" + const val COL_OWNED_STOCK_COUNT = "owned_stock_count" const val COL_OWNED_AVERAGE_STOCK_PRICE = "owned_average_price" const val COL_STOCK_PRICE = "stock_price" const val COL_COUNT = "count" @@ -42,6 +45,7 @@ fun Trade.asEntity() = TradeEntity( id = id, gameId = gameId, + ownedStockCount = ownedStockCount, ownedAverageStockPrice = ownedAverageStockPrice, stockPrice = stockPrice, count = count, @@ -54,6 +58,7 @@ fun TradeEntity.asDomainModel() = Trade( id = id, gameId = gameId, + ownedStockCount = ownedStockCount, ownedAverageStockPrice = ownedAverageStockPrice, stockPrice = stockPrice, count = count, diff --git a/data/src/main/java/com/yessorae/data/source/network/polygon/model/chart/TickDto.kt b/data/src/main/java/com/yessorae/data/source/network/polygon/model/chart/TickDto.kt index cc1a8fa..ed8ad1c 100644 --- a/data/src/main/java/com/yessorae/data/source/network/polygon/model/chart/TickDto.kt +++ b/data/src/main/java/com/yessorae/data/source/network/polygon/model/chart/TickDto.kt @@ -3,6 +3,7 @@ package com.yessorae.data.source.network.polygon.model.chart import com.yessorae.data.util.toLocalDateTime import com.yessorae.domain.entity.tick.Tick import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -28,10 +29,10 @@ data class TickDto( internal fun TickDto.asDomainModel() = Tick( - openPrice = Money(openPrice), - closePrice = Money(closePrice), - maxPrice = Money(maxPrice), - minPrice = Money(minPrice), + openPrice = openPrice.asMoney(), + closePrice = closePrice.asMoney(), + maxPrice = maxPrice.asMoney(), + minPrice = minPrice.asMoney(), transactionCount = transactionCount, startTimestamp = startTimestamp.toLocalDateTime(), tradingVolume = tradingVolume.toInt(), diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index ad56a3c..295a2ef 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -17,4 +17,5 @@ dependencies { implementation(Dependency.PlatformIndependent.KOTLINX_COROUTINSE) implementation(Dependency.Common.KOTOIN_SERIALIZATION_JSON) implementation(Dependency.Common.PAGING_COMMON) + testImplementation(Dependency.Common.JUNIT) } diff --git a/domain/src/main/java/com/yessorae/domain/entity/ChartGame.kt b/domain/src/main/java/com/yessorae/domain/entity/ChartGame.kt index d1afd4a..c42b336 100644 --- a/domain/src/main/java/com/yessorae/domain/entity/ChartGame.kt +++ b/domain/src/main/java/com/yessorae/domain/entity/ChartGame.kt @@ -2,6 +2,7 @@ package com.yessorae.domain.entity import com.yessorae.domain.entity.trade.Trade import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney data class ChartGame( val id: Long = 0, @@ -21,13 +22,13 @@ data class ChartGame( val isQuit: Boolean, // 현재 보유 주식 수량 val totalStockCount: Int, - // 현재 보유 주식 가격의 총합 - val totalStockPrice: Money, // 현재 보유 주식 평단가 val averageStockPrice: Money, - // 누적 수익 + // 누적 실현 손익 val accumulatedTotalProfit: Money ) { + // 현재 보유 주식 가격의 총합 + val totalStockPrice: Money = closeStockPrice * totalStockCount // 누적 수익률 val accumulatedRateOfProfit: Double = (accumulatedTotalProfit / startBalance).value @@ -41,10 +42,11 @@ data class ChartGame( // 정상종료이든 강제종료이든 종료된 경우 true val isGameEnd: Boolean = isQuit || isGameComplete - internal fun getNextTurnResult(): ChartGame { + internal fun getNextTurnResult(closeStockPrice: Money): ChartGame { val nextTurn = currentTurn + 1 return this.copy( - currentTurn = nextTurn + currentTurn = nextTurn, + closeStockPrice = closeStockPrice ) } @@ -56,23 +58,16 @@ data class ChartGame( } return copy( - currentBalance = currentBalance + Money.of( + currentBalance = currentBalance + if (newTrade.type.isBuy()) { - -newTrade.totalTradeMoney.value + -(newTrade.totalTradeMoney + newTrade.commission).value } else { - newTrade.totalTradeMoney.value - } - ), + (newTrade.totalTradeMoney - newTrade.commission).value + }.asMoney(), totalStockCount = newTotalStockCount, - totalStockPrice = totalStockPrice + Money.of( - if (newTrade.type.isBuy()) { - newTrade.totalTradeMoney.value - } else { - -newTrade.totalTradeMoney.value - } - ), averageStockPrice = if (newTrade.type.isBuy()) { - (totalStockPrice + newTrade.totalTradeMoney) / newTotalStockCount + (averageStockPrice * totalStockCount + newTrade.totalTradeMoney) / + newTotalStockCount } else { averageStockPrice }, @@ -86,7 +81,6 @@ data class ChartGame( currentBalance = startBalance, closeStockPrice = closeStockPrice, totalStockCount = 0, - totalStockPrice = Money.ZERO, averageStockPrice = Money.ZERO, accumulatedTotalProfit = Money.ZERO ) @@ -116,7 +110,6 @@ data class ChartGame( closeStockPrice = closeStockPrice, isQuit = false, totalStockCount = 0, - totalStockPrice = Money.ZERO, averageStockPrice = Money.ZERO, accumulatedTotalProfit = Money.ZERO ) diff --git a/domain/src/main/java/com/yessorae/domain/entity/User.kt b/domain/src/main/java/com/yessorae/domain/entity/User.kt index 91c64de..b622861 100644 --- a/domain/src/main/java/com/yessorae/domain/entity/User.kt +++ b/domain/src/main/java/com/yessorae/domain/entity/User.kt @@ -2,6 +2,7 @@ package com.yessorae.domain.entity import com.yessorae.domain.common.DefaultValues import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney import kotlinx.serialization.Serializable @Serializable @@ -39,7 +40,7 @@ data class User( ): User { val oldTotalGameCount = winCount + loseCount return User( - balance = balance + Money(profit), + balance = balance + profit.asMoney(), winCount = winCount + (if (profit > 0) 1 else 0), loseCount = loseCount + (if (profit < 0) 1 else 0), averageRateOfProfit = diff --git a/domain/src/main/java/com/yessorae/domain/entity/trade/Trade.kt b/domain/src/main/java/com/yessorae/domain/entity/trade/Trade.kt index 1ca8ce8..ac5de87 100644 --- a/domain/src/main/java/com/yessorae/domain/entity/trade/Trade.kt +++ b/domain/src/main/java/com/yessorae/domain/entity/trade/Trade.kt @@ -1,11 +1,14 @@ package com.yessorae.domain.entity.trade import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney data class Trade( val id: Long = 0, // 차트게임 아이디 val gameId: Long, + // 현재 가지고 있는 주식 개수 + val ownedStockCount: Int, // 현재 가지고 있는 가격 val ownedAverageStockPrice: Money, // 1 주당 가격 @@ -24,18 +27,22 @@ data class Trade( val totalTradeMoney: Money = stockPrice * count // 수수료 - val commission: Money = totalTradeMoney * commissionRate + val commission: Money = totalTradeMoney * commissionRate / 100 // 실현 손익, 매도할 때만 유효 val profit: Money = if (type.isBuy()) { - Money(0.0) // TODO::CT-52 버그 수정 필요. 도메인 모델 Unit Testing 브랜치에서 수정 예정. + (-commission.value).asMoney() } else { - ((stockPrice - ownedAverageStockPrice) * count) - commission + val sellProfit = totalTradeMoney - commission + val totalOwnedStockPrice = ownedAverageStockPrice * count + + sellProfit - totalOwnedStockPrice } companion object { internal fun new( gameId: Long, + ownedStockCount: Int, ownedAverageStockPrice: Money, stockPrice: Money, count: Int, @@ -45,6 +52,7 @@ data class Trade( ): Trade { return Trade( gameId = gameId, + ownedStockCount = ownedStockCount, ownedAverageStockPrice = ownedAverageStockPrice, stockPrice = stockPrice, count = count, diff --git a/domain/src/main/java/com/yessorae/domain/entity/trade/TradeType.kt b/domain/src/main/java/com/yessorae/domain/entity/trade/TradeType.kt index b0ec10a..98fdbff 100644 --- a/domain/src/main/java/com/yessorae/domain/entity/trade/TradeType.kt +++ b/domain/src/main/java/com/yessorae/domain/entity/trade/TradeType.kt @@ -1,9 +1,9 @@ package com.yessorae.domain.entity.trade enum class TradeType { - Buy, - Sell; + BUY, + SELL; - fun isBuy(): Boolean = this == Buy - fun isSell(): Boolean = this == Sell + fun isBuy(): Boolean = this == BUY + fun isSell(): Boolean = this == SELL } diff --git a/domain/src/main/java/com/yessorae/domain/entity/value/Money.kt b/domain/src/main/java/com/yessorae/domain/entity/value/Money.kt index da45643..7fdb9d4 100644 --- a/domain/src/main/java/com/yessorae/domain/entity/value/Money.kt +++ b/domain/src/main/java/com/yessorae/domain/entity/value/Money.kt @@ -9,41 +9,47 @@ data class Money( // TODO::LATER 화폐단위 바꿔주는 함수 추가 operator fun plus(other: Money): Money { - return Money(value + other.value) + return (value + other.value).asMoney() } operator fun times(other: Money): Money { - return Money(value * other.value) + return (value * other.value).asMoney() } operator fun times(count: Int): Money { - return Money(value * count) + return (value * count).asMoney() } operator fun times(count: Double): Money { - return Money(value * count) + return (value * count).asMoney() } operator fun minus(other: Money): Money { - return Money(value - other.value) + return (value - other.value).asMoney() } operator fun div(other: Money): Money { - return Money(value / other.value) + return (value / other.value).asMoney() } operator fun div(other: Double): Money { - return Money(value / other) + return (value / other).asMoney() } operator fun div(other: Int): Money { - return Money(value / other) + return (value / other).asMoney() } companion object { - val ZERO = Money(0.0) + val ZERO = 0.asMoney() fun of(value: Double): Money { return Money(value) } } } + +fun Int.asMoney() = Money.of(value = this.toDouble()) + +fun Float.asMoney() = Money.of(value = this.toDouble()) + +fun Double.asMoney() = Money.of(value = this) diff --git a/domain/src/main/java/com/yessorae/domain/usecase/TradeStockUseCase.kt b/domain/src/main/java/com/yessorae/domain/usecase/TradeStockUseCase.kt index 1bba6c2..03b0605 100644 --- a/domain/src/main/java/com/yessorae/domain/usecase/TradeStockUseCase.kt +++ b/domain/src/main/java/com/yessorae/domain/usecase/TradeStockUseCase.kt @@ -22,6 +22,7 @@ class TradeStockUseCase @Inject constructor( with(param) { val trade = Trade.new( gameId = gameId, + ownedStockCount = ownedStockCount, ownedAverageStockPrice = ownedAverageStockPrice, stockPrice = stockPrice, count = count, @@ -39,6 +40,7 @@ class TradeStockUseCase @Inject constructor( data class Param( val gameId: Long, + val ownedStockCount: Int, val ownedAverageStockPrice: Money, val stockPrice: Money, val count: Int, diff --git a/domain/src/main/java/com/yessorae/domain/usecase/UpdateNextTickUseCase.kt b/domain/src/main/java/com/yessorae/domain/usecase/UpdateNextTickUseCase.kt index c01ac4e..3f085a1 100644 --- a/domain/src/main/java/com/yessorae/domain/usecase/UpdateNextTickUseCase.kt +++ b/domain/src/main/java/com/yessorae/domain/usecase/UpdateNextTickUseCase.kt @@ -5,6 +5,7 @@ import com.yessorae.domain.common.delegateEmptyResultFlow import com.yessorae.domain.entity.User 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 @@ -12,6 +13,7 @@ import kotlinx.coroutines.flow.flow class UpdateNextTickUseCase @Inject constructor( private val chartGameRepository: ChartGameRepository, + private val chartRepository: ChartRepository, private val userRepository: UserRepository ) { operator fun invoke(gameId: Long): Flow> = @@ -24,7 +26,20 @@ class UpdateNextTickUseCase @Inject constructor( ) } - val newChartGame = oldChartGame.getNextTurnResult() + val chart = chartRepository.fetchChart(gameId = gameId) + val lastVisibleTickIndex = (chart.ticks.size - 1) - + (oldChartGame.totalTurn - (oldChartGame.currentTurn + 1)) + + if (lastVisibleTickIndex < 0) { + throw ChartGameException.CanNotCreateChartGame( + message = "can't change chart because new chart has not enough ticks" + ) + } + + // getNextTurnResult 에 Chart 를 전달하고 인덱스 검사하는 부분을 getNextTurnResult 에 포함할지 고민중 + val newChartGame = oldChartGame.getNextTurnResult( + closeStockPrice = chart.ticks[lastVisibleTickIndex].closePrice + ) if (newChartGame.isGameEnd) { val oldUser: User = userRepository.fetchUser() diff --git a/domain/src/test/java/com/yessorae/domain/model/ChartGameTest.kt b/domain/src/test/java/com/yessorae/domain/model/ChartGameTest.kt new file mode 100644 index 0000000..43d5f83 --- /dev/null +++ b/domain/src/test/java/com/yessorae/domain/model/ChartGameTest.kt @@ -0,0 +1,174 @@ +package com.yessorae.domain.model + +import com.yessorae.domain.entity.ChartGame +import com.yessorae.domain.entity.trade.Trade +import com.yessorae.domain.entity.trade.TradeType +import com.yessorae.domain.entity.value.Money +import org.junit.Assert.assertEquals +import org.junit.Test + +class ChartGameTest { + @Test + fun chart_game_profit_sell_result() { + // 익절. 평단가 40,000원에 10개를 가지고 있다가 50,000원에 5개를 팔았을 때 + val trade: Trade = createTestTrade( + type = TradeType.SELL, + ownedAverageStockPrice = Money.of(40_000.0), + ownedStockCount = 10, + stockPrice = Money.of(50_000.0), + count = 5, + commissionRate = 0.1 + ) + val sut: ChartGame = createTestChartGame( + // 500만원 있는 사람이 + startBalance = Money.of(5_000_000.0), + // 과거 4만원짜리 주식 10주 사서 현재 잔액은 460만원이고 + totalStockCount = 10, + currentBalance = Money.of(4_600_000.0), + // 평단가는 4만원 + averageStockPrice = Money.of(40_000.0), + // 현재가격은 5만원 + closeStockPrice = Money.of(50_000.0), + // 과거 4만원짜리 주식 10주 사서 발생한 수수료 손실금이 4백원인 상태 + accumulatedTotalProfit = Money.of(-400.0) + ) + + val result: ChartGame = sut.getTradeResult(newTrade = trade) + + assertEquals( + sut.copy( + currentBalance = Money.of(4_849_750.0), + totalStockCount = 5, + averageStockPrice = Money.of(40_000.0), + accumulatedTotalProfit = Money.of(49_350.0) + ), + result + ) + } + + @Test + fun chart_game_stop_loss_sell_result() { + // 손절. 평단가 50,000원에 10개를 가지고 있다가 40,000원에 5개를 팔았을 때 + val trade: Trade = createTestTrade( + type = TradeType.SELL, + ownedAverageStockPrice = Money.of(50_000.0), + ownedStockCount = 10, + stockPrice = Money.of(40_000.0), + count = 5, + commissionRate = 0.1 + ) + val sut: ChartGame = createTestChartGame( + // 500만원 있는 사람이 + startBalance = Money.of(5_000_000.0), + // 5만원짜리 주식 10주 사서 현재 잔액은 450만원이고 평단가는 5만원 현재가격은 4만원 + totalStockCount = 10, + currentBalance = Money.of(4_500_000.0), + averageStockPrice = Money.of(50_000.0), + closeStockPrice = Money.of(40_000.0), + // 5만원짜리 주식 10주 사서 발생한 수수료 손실금 + accumulatedTotalProfit = Money.of(-500.0) + ) + + val result: ChartGame = sut.getTradeResult(newTrade = trade) + + assertEquals( + sut.copy( + currentBalance = Money.of(4_699_800.0), + totalStockCount = 5, + averageStockPrice = Money.of(50_000.0), + accumulatedTotalProfit = Money.of(-50_700.0) + ), + result + ) + } + + @Test + fun chart_game_buy_result() { + // 5만원에 100주 매수. + val trade: Trade = createTestTrade( + type = TradeType.BUY, + stockPrice = Money.of(50_000.0), + commissionRate = 0.1, + count = 10 + ) + val sut: ChartGame = createTestChartGame( + // 500만원 있는 사람이 + startBalance = Money.of(5_000_000.0), + // 과거 4만원짜리 주식 10주 사서 현재 잔액은 460만원이고 + totalStockCount = 10, + currentBalance = Money.of(4_600_000.0), + // 평단가는 4만원 + averageStockPrice = Money.of(40_000.0), + // 현재가격은 5만원 + closeStockPrice = Money.of(50_000.0), + // 과거 4만원짜리 주식 10주 사서 발생한 수수료 손실금이 400원인 상태 + accumulatedTotalProfit = Money.of(-400.0) + ) + + val result: ChartGame = sut.getTradeResult(newTrade = trade) + + assertEquals( + sut.copy( + currentBalance = Money.of(4_099_500.0), + totalStockCount = 20, + averageStockPrice = Money.of(45_000.0), + accumulatedTotalProfit = Money.of(-900.0) + ), + result + ) + } + + @Test + fun chart_game_next_turn_result() { + val sut: ChartGame = createTestChartGame( + currentTurn = 1 + ) + + val result: ChartGame = sut.getNextTurnResult( + closeStockPrice = Money.of(50_000.0) + ) + + assertEquals( + sut.copy( + currentTurn = 2, + closeStockPrice = Money.of(50_000.0) + ), + result + ) + } + + @Test + fun chart_game_chart_change_result() { + val sut: ChartGame = createTestChartGame( + startBalance = Money.of(100_000.0) + ) + + val result: ChartGame = sut.getChartChangeResult( + closeStockPrice = Money.of(50_000.0) + ) + + assertEquals( + sut.copy( + currentTurn = ChartGame.START_TURN, + currentBalance = Money.of(100_000.0), + closeStockPrice = Money.of(50_000.0), + totalStockCount = 0, + averageStockPrice = Money.ZERO, + accumulatedTotalProfit = Money.ZERO + ), + result + ) + } + + @Test + fun chart_game_quit_result() { + val sut: ChartGame = createTestChartGame() + + val result: ChartGame = sut.getQuitResult() + + assertEquals( + sut.copy(isQuit = true), + result + ) + } +} diff --git a/domain/src/test/java/com/yessorae/domain/model/TestData.kt b/domain/src/test/java/com/yessorae/domain/model/TestData.kt new file mode 100644 index 0000000..f732837 --- /dev/null +++ b/domain/src/test/java/com/yessorae/domain/model/TestData.kt @@ -0,0 +1,114 @@ +package com.yessorae.domain.model + +import com.yessorae.domain.entity.Chart +import com.yessorae.domain.entity.ChartGame +import com.yessorae.domain.entity.User +import com.yessorae.domain.entity.tick.Tick +import com.yessorae.domain.entity.tick.TickUnit +import com.yessorae.domain.entity.trade.Trade +import com.yessorae.domain.entity.trade.TradeType +import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney +import java.time.LocalDateTime +import kotlin.random.Random + +private const val TEST_CHART_GAME_ID = 1L +private const val TEST_CHART_ID = 2L +private const val TEST_TICKER_SYMBOL = "AAPL" +private const val TICK_COUNT = 10 +private val testStartDate: LocalDateTime = LocalDateTime.of(2024, 5, 29, 0, 0) +private val testEndDate: LocalDateTime = LocalDateTime.of(2022, 5, 29, 0, 0) +private const val DEFAULT_INT = Int.MAX_VALUE +private const val DEFAULT_DOUBLE = Double.MAX_VALUE +private val defaultMoney = DEFAULT_DOUBLE.asMoney() + +val baseClosePrice: Money = 150.asMoney() +val testClosePrice: Money = baseClosePrice * TICK_COUNT + +fun createTestTick(index: Int) = + Tick( + openPrice = 100.asMoney() * index, + maxPrice = 200.asMoney() * index, + minPrice = 50.asMoney() * index, + closePrice = baseClosePrice * index, + transactionCount = 1000 * index, + startTimestamp = testStartDate.plusDays(index.toLong()), + tradingVolume = 1000 * index, + volumeWeightedAveragePrice = 150.asMoney() * index + ) + +fun createTestTicks() = (1..TICK_COUNT).map { index -> createTestTick(index = index) } + +fun createTestChart( + id: Long = TEST_CHART_ID, + tickerSymbol: String = TEST_TICKER_SYMBOL, + startDateTime: LocalDateTime = testStartDate, + endDateTime: LocalDateTime = testEndDate, + ticks: List = createTestTicks(), + tickUnit: TickUnit = TickUnit.DAY +) = Chart( + id = id, + tickerSymbol = tickerSymbol, + startDateTime = startDateTime, + endDateTime = endDateTime, + ticks = ticks, + tickUnit = tickUnit +) + +fun createTestChartGame( + id: Long = TEST_CHART_GAME_ID, + chartId: Long = TEST_CHART_ID, + currentTurn: Int = DEFAULT_INT, + totalTurn: Int = DEFAULT_INT, + startBalance: Money = defaultMoney, + currentBalance: Money = defaultMoney, + closeStockPrice: Money = defaultMoney, + isQuit: Boolean = Random.nextBoolean(), + totalStockCount: Int = DEFAULT_INT, + averageStockPrice: Money = defaultMoney, + accumulatedTotalProfit: Money = defaultMoney +) = ChartGame( + id = id, + chartId = chartId, + currentTurn = currentTurn, + totalTurn = totalTurn, + startBalance = startBalance, + currentBalance = currentBalance, + closeStockPrice = closeStockPrice, + isQuit = isQuit, + totalStockCount = totalStockCount, + averageStockPrice = averageStockPrice, + accumulatedTotalProfit = accumulatedTotalProfit +) + +fun createTestTrade( + gameId: Long = TEST_CHART_GAME_ID, + ownedStockCount: Int = DEFAULT_INT, + ownedAverageStockPrice: Money = defaultMoney, + stockPrice: Money = defaultMoney, + count: Int = DEFAULT_INT, + turn: Int = DEFAULT_INT, + type: TradeType = TradeType.BUY, + commissionRate: Double = DEFAULT_DOUBLE +) = Trade( + gameId = gameId, + ownedStockCount = ownedStockCount, + ownedAverageStockPrice = ownedAverageStockPrice, + stockPrice = stockPrice, + count = count, + turn = turn, + type = type, + commissionRate = commissionRate +) + +fun createTestUser( + balance: Money = defaultMoney, + winCount: Int = DEFAULT_INT, + loseCount: Int = DEFAULT_INT, + averageRateOfProfit: Double = DEFAULT_DOUBLE +) = User( + balance = balance, + winCount = winCount, + loseCount = loseCount, + averageRateOfProfit = averageRateOfProfit +) diff --git a/domain/src/test/java/com/yessorae/domain/model/TradeTest.kt b/domain/src/test/java/com/yessorae/domain/model/TradeTest.kt new file mode 100644 index 0000000..f4c0cb5 --- /dev/null +++ b/domain/src/test/java/com/yessorae/domain/model/TradeTest.kt @@ -0,0 +1,52 @@ +package com.yessorae.domain.model + +import com.yessorae.domain.entity.trade.TradeType +import com.yessorae.domain.entity.value.asMoney +import org.junit.Assert.assertEquals +import org.junit.Test + +class TradeTest { + @Test + fun profit_with_selling_part_of_owned_stock_at_higher_price() { + val trade = createTestTrade( + type = TradeType.SELL, + ownedAverageStockPrice = 40_000.asMoney(), + ownedStockCount = 10, + stockPrice = 50_000.asMoney(), + count = 5, + commissionRate = 0.1 + ) + + assertEquals(250_000.asMoney(), trade.totalTradeMoney) + assertEquals(250.asMoney(), trade.commission) + assertEquals(49_750.asMoney(), trade.profit) + } + + @Test + fun stop_loss_with_selling_part_of_owned_stock_at_higher_price() { + val trade = createTestTrade( + type = TradeType.SELL, + ownedAverageStockPrice = 50_000.asMoney(), + ownedStockCount = 10, + stockPrice = 40_000.asMoney(), + count = 5, + commissionRate = 0.1 + ) + + assertEquals(200_000.asMoney(), trade.totalTradeMoney) + assertEquals(200.asMoney(), trade.commission) + assertEquals((-50_200.0).asMoney(), trade.profit) + } + + @Test + fun loss_with_buying_commission_rate() { + val trade = createTestTrade( + type = TradeType.BUY, + stockPrice = 50_000.asMoney(), + commissionRate = 0.1, + count = 100 + ) + + assertEquals((-5_000).asMoney(), trade.profit) + } +} diff --git a/domain/src/test/java/com/yessorae/domain/model/UserTest.kt b/domain/src/test/java/com/yessorae/domain/model/UserTest.kt new file mode 100644 index 0000000..2d7c3b2 --- /dev/null +++ b/domain/src/test/java/com/yessorae/domain/model/UserTest.kt @@ -0,0 +1,109 @@ +package com.yessorae.domain.model + +import com.yessorae.domain.entity.User +import com.yessorae.domain.entity.value.Money +import junit.framework.Assert.assertEquals +import org.junit.Test + +class UserTest { + @Test + fun initial_user_winning_rate_is_zero() { + // createInitialUser는 운영코드에서 사용되는 함수 + val sut: User = User.createInitialUser() + + assertEquals(0.0, sut.rateOfWinning) + } + + @Test + fun initial_user_losing_rate_is_zero() { + // createInitialUser는 운영코드에서 사용되는 함수 + val sut: User = User.createInitialUser() + + assertEquals(0.0, sut.rateOfLosing) + } + + @Test + fun user_winning_rate() { + val sut: User = createTestUser( + winCount = 3, + loseCount = 1 + ) + + assertEquals(0.75, sut.rateOfWinning) + } + + @Test + fun user_losing_rate() { + val sut: User = createTestUser( + winCount = 1, + loseCount = 3 + ) + + assertEquals(0.75, sut.rateOfLosing) + } + + @Test + fun user_quite_game_result() { + val sut: User = createTestUser( + winCount = 1, + loseCount = 2 + ) + + val result: User = sut.quiteGame() + + assertEquals( + sut.copy( + loseCount = 3 + ), + result + ) + } + + @Test + fun user_finish_game_with_wining_result() { + val sut: User = createTestUser( + balance = Money.of(100_000.0), + winCount = 98, + loseCount = 1, + averageRateOfProfit = 2.0 + ) + + val result: User = sut.finishGame( + profit = 100.0, + rateOfProfit = 1.0 + ) + + assertEquals( + sut.copy( + balance = Money.of(100_100.0), + winCount = 99, + averageRateOfProfit = 1.99 + ), + result + ) + } + + @Test + fun user_finish_game_with_losing_result() { + val sut: User = createTestUser( + balance = Money.of(100_000.0), + winCount = 98, + loseCount = 1, + averageRateOfProfit = 2.0 + ) + + val result: User = sut.finishGame( + profit = -100.0, + rateOfProfit = -1.0 + ) + + assertEquals( + sut.copy( + balance = Money.of(99_900.0), + loseCount = 2, + averageRateOfProfit = 1.97 + ), + result + ) + } +} diff --git a/presentation/src/main/java/com/yessorae/presentation/ui/designsystem/util/DisplayTextProvider.kt b/presentation/src/main/java/com/yessorae/presentation/ui/designsystem/util/DisplayTextProvider.kt index 264cafb..01f048d 100644 --- a/presentation/src/main/java/com/yessorae/presentation/ui/designsystem/util/DisplayTextProvider.kt +++ b/presentation/src/main/java/com/yessorae/presentation/ui/designsystem/util/DisplayTextProvider.kt @@ -26,14 +26,14 @@ fun provideTickUnitText(tickUnit: TickUnit) = @Composable fun TradeType.asText(): String = when (this) { - TradeType.Buy -> stringResource(id = R.string.common_buy) - TradeType.Sell -> stringResource(id = R.string.common_sell) + TradeType.BUY -> stringResource(id = R.string.common_buy) + TradeType.SELL -> stringResource(id = R.string.common_sell) } fun TradeType.asColor(): Color = when (this) { - TradeType.Buy -> StockUpColor - TradeType.Sell -> StockDownColor + TradeType.BUY -> StockUpColor + TradeType.SELL -> StockDownColor } fun Money.asDefaultDisplayText(): String = "%.2f".format(value) diff --git a/presentation/src/main/java/com/yessorae/presentation/ui/screen/chartgame/ChartGameViewModel.kt b/presentation/src/main/java/com/yessorae/presentation/ui/screen/chartgame/ChartGameViewModel.kt index fc48aba..0318568 100644 --- a/presentation/src/main/java/com/yessorae/presentation/ui/screen/chartgame/ChartGameViewModel.kt +++ b/presentation/src/main/java/com/yessorae/presentation/ui/screen/chartgame/ChartGameViewModel.kt @@ -150,6 +150,7 @@ class ChartGameViewModel @Inject constructor( is ChartGameScreenUserAction.ClickBuyButton -> { showBuyOrderUi( gameId = gameId, + ownedStockCount = ownedStockCount, ownedAverageStockPrice = ownedAverageStockPrice, currentBalance = currentBalance, currentStockPrice = currentStockPrice, @@ -189,6 +190,7 @@ class ChartGameViewModel @Inject constructor( private fun showBuyOrderUi( gameId: Long, + ownedStockCount: Int, ownedAverageStockPrice: Money, currentBalance: Money, currentStockPrice: Money, @@ -204,6 +206,7 @@ class ChartGameViewModel @Inject constructor( onUserAction = { userAction -> handleBuyingOrderUiUserAction( gameId = gameId, + ownedStockCount = ownedStockCount, ownedAverageStockPrice = ownedAverageStockPrice, currentStockPrice = currentStockPrice, currentTurn = currentTurn, @@ -219,6 +222,7 @@ class ChartGameViewModel @Inject constructor( private fun handleBuyingOrderUiUserAction( gameId: Long, maxAvailableStockCount: Int, + ownedStockCount: Int, ownedAverageStockPrice: Money, currentStockPrice: Money, currentTurn: Int, @@ -246,11 +250,12 @@ class ChartGameViewModel @Inject constructor( tradeStock( TradeStockUseCase.Param( gameId = gameId, + ownedStockCount = ownedStockCount, ownedAverageStockPrice = ownedAverageStockPrice, stockPrice = currentStockPrice, count = count.toInt(), turn = currentTurn, - type = TradeType.Buy + type = TradeType.BUY ) ) } @@ -366,11 +371,12 @@ class ChartGameViewModel @Inject constructor( tradeStock( tradeStockParam = TradeStockUseCase.Param( gameId = gameId, + ownedStockCount = ownedStockCount, ownedAverageStockPrice = ownedAverageStockPrice, stockPrice = currentStockPrice, count = count.toInt(), turn = currentTurn, - type = TradeType.Sell + type = TradeType.SELL ) ) } diff --git a/presentation/src/main/java/com/yessorae/presentation/ui/screen/chartgamehistory/component/ChartGameHistoryListItem.kt b/presentation/src/main/java/com/yessorae/presentation/ui/screen/chartgamehistory/component/ChartGameHistoryListItem.kt index 10ed0bc..0b3c7ac 100644 --- a/presentation/src/main/java/com/yessorae/presentation/ui/screen/chartgamehistory/component/ChartGameHistoryListItem.kt +++ b/presentation/src/main/java/com/yessorae/presentation/ui/screen/chartgamehistory/component/ChartGameHistoryListItem.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.yessorae.domain.entity.tick.TickUnit -import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney import com.yessorae.presentation.ui.designsystem.theme.Dimen import com.yessorae.presentation.ui.designsystem.theme.StockDownColor import com.yessorae.presentation.ui.designsystem.theme.StockUpColor @@ -92,7 +92,7 @@ fun PreviewChartGameHistoryListItem() { ticker = "AAPL", totalTurn = 10, tickUnit = TickUnit.DAY, - totalProfit = Money(10.0), + totalProfit = 10.asMoney(), isTotalProfitPositive = true, startDate = LocalDateTime.of(2024, 1, 1, 0, 0), endDate = LocalDateTime.of(2026, 1, 1, 0, 0) diff --git a/presentation/src/main/java/com/yessorae/presentation/ui/screen/home/model/HomeScreenState.kt b/presentation/src/main/java/com/yessorae/presentation/ui/screen/home/model/HomeScreenState.kt index a19cf81..14d031a 100644 --- a/presentation/src/main/java/com/yessorae/presentation/ui/screen/home/model/HomeScreenState.kt +++ b/presentation/src/main/java/com/yessorae/presentation/ui/screen/home/model/HomeScreenState.kt @@ -3,6 +3,7 @@ package com.yessorae.presentation.ui.screen.home.model import com.yessorae.domain.common.DefaultValues import com.yessorae.domain.entity.tick.TickUnit import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney data class HomeState( val userInfoUi: UserInfoUi = UserInfoUi(), @@ -21,7 +22,7 @@ data class SettingInfoUi( ) data class UserInfoUi( - val currentBalance: Money = Money(0.0), + val currentBalance: Money = 0.asMoney(), val winCount: Int = 0, val loseCount: Int = 0, val averageRateOfProfit: Float = 0f, diff --git a/presentation/src/main/java/com/yessorae/presentation/ui/screen/tradehistory/TradeHistoryScreen.kt b/presentation/src/main/java/com/yessorae/presentation/ui/screen/tradehistory/TradeHistoryScreen.kt index af17de4..5fd5a7c 100644 --- a/presentation/src/main/java/com/yessorae/presentation/ui/screen/tradehistory/TradeHistoryScreen.kt +++ b/presentation/src/main/java/com/yessorae/presentation/ui/screen/tradehistory/TradeHistoryScreen.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import com.yessorae.domain.entity.trade.TradeType -import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney import com.yessorae.presentation.R import com.yessorae.presentation.ui.designsystem.theme.Dimen import com.yessorae.presentation.ui.designsystem.util.DevicePreviews @@ -114,22 +114,22 @@ fun TradeHistoryScreenPreview() { TradeHistoryListItem( id = 1, turn = 1, - tradeType = TradeType.Buy, - stockPrice = Money(123.0), + tradeType = TradeType.BUY, + stockPrice = 123.asMoney(), count = 10, - totalPrice = Money(1230.0), - commission = Money(12.3), - profit = Money(-123.0) + totalPrice = 1230.asMoney(), + commission = 12.3.asMoney(), + profit = (-123).asMoney() ), TradeHistoryListItem( id = 2, turn = 2, - tradeType = TradeType.Sell, - stockPrice = Money(125.0), + tradeType = TradeType.SELL, + stockPrice = 125.asMoney(), count = 10, - totalPrice = Money(1250.0), - commission = Money(12.4), - profit = Money(7.6) + totalPrice = 1250.asMoney(), + commission = 12.4.asMoney(), + profit = 7.6.asMoney() ) ) ), diff --git a/presentation/src/main/java/com/yessorae/presentation/ui/screen/tradehistory/component/TradeHistoryListItem.kt b/presentation/src/main/java/com/yessorae/presentation/ui/screen/tradehistory/component/TradeHistoryListItem.kt index 29b5bed..8ab0622 100644 --- a/presentation/src/main/java/com/yessorae/presentation/ui/screen/tradehistory/component/TradeHistoryListItem.kt +++ b/presentation/src/main/java/com/yessorae/presentation/ui/screen/tradehistory/component/TradeHistoryListItem.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.yessorae.domain.entity.trade.TradeType -import com.yessorae.domain.entity.value.Money +import com.yessorae.domain.entity.value.asMoney import com.yessorae.presentation.R import com.yessorae.presentation.ui.designsystem.theme.StockDownColor import com.yessorae.presentation.ui.designsystem.theme.StockUpColor @@ -195,12 +195,12 @@ fun TradeHistoryListItemPreview() { tradeHistory = TradeHistoryListItem( id = 1, turn = 1, - tradeType = TradeType.Buy, - stockPrice = Money(1000.0), + tradeType = TradeType.BUY, + stockPrice = 1000.asMoney(), count = 10, - totalPrice = Money(10000.0), - commission = Money(100.0), - profit = Money(100.0) + totalPrice = 10000.asMoney(), + commission = 100.asMoney(), + profit = 100.asMoney() ), totalTurn = 10 )