diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/copytrading/leaderpool/LeaderPoolController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/copytrading/leaderpool/LeaderPoolController.kt index d3b24be..b098641 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/copytrading/leaderpool/LeaderPoolController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/copytrading/leaderpool/LeaderPoolController.kt @@ -6,6 +6,7 @@ import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolAlreadyE import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolConfirmRequiredException import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolDuplicateTrialConfigException import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolNotFoundException +import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolResearchCandidateNotReadyException import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolService import org.slf4j.LoggerFactory import org.springframework.context.MessageSource @@ -146,6 +147,7 @@ class LeaderPoolController( is LeaderPoolAlreadyExistsException -> ErrorCode.LEADER_POOL_ALREADY_EXISTS is LeaderPoolDuplicateTrialConfigException -> ErrorCode.LEADER_POOL_DUPLICATE_TRIAL_CONFIG is LeaderPoolConfirmRequiredException -> ErrorCode.LEADER_POOL_CONFIRM_REQUIRED + is LeaderPoolResearchCandidateNotReadyException -> ErrorCode.LEADER_RESEARCH_CANDIDATE_NOT_READY is IllegalArgumentException -> when (e.message) { "账户不存在" -> ErrorCode.ACCOUNT_NOT_FOUND "Leader 不存在" -> ErrorCode.LEADER_NOT_FOUND @@ -166,6 +168,7 @@ class LeaderPoolController( errorCode == ErrorCode.LEADER_POOL_ALREADY_EXISTS || errorCode == ErrorCode.LEADER_POOL_DUPLICATE_TRIAL_CONFIG || errorCode == ErrorCode.LEADER_POOL_CONFIRM_REQUIRED || + errorCode == ErrorCode.LEADER_RESEARCH_CANDIDATE_NOT_READY || errorCode == ErrorCode.ACCOUNT_NOT_FOUND || errorCode == ErrorCode.LEADER_NOT_FOUND } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolExceptions.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolExceptions.kt index 5056e2d..268db54 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolExceptions.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolExceptions.kt @@ -7,3 +7,7 @@ class LeaderPoolAlreadyExistsException(message: String = "Leader 已在池子中 class LeaderPoolDuplicateTrialConfigException(message: String = "该账户已存在此 Leader 的跟单配置") : RuntimeException(message) class LeaderPoolConfirmRequiredException(message: String = "立即启用试跟配置需要显式确认") : RuntimeException(message) + +class LeaderPoolResearchCandidateNotReadyException( + message: String = "研究候选尚未进入试跟建议状态" +) : RuntimeException(message) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolService.kt index 3bfd0b2..52dce73 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolService.kt @@ -5,6 +5,7 @@ import com.wrbug.polymarketbot.entity.CopyTrading import com.wrbug.polymarketbot.entity.Leader import com.wrbug.polymarketbot.entity.LeaderPool import com.wrbug.polymarketbot.enums.LeaderPoolStatus +import com.wrbug.polymarketbot.enums.LeaderResearchState import com.wrbug.polymarketbot.repository.AccountRepository import com.wrbug.polymarketbot.repository.CopyTradingRepository import com.wrbug.polymarketbot.repository.LeaderPoolRepository @@ -207,6 +208,15 @@ class LeaderPoolService( } return try { + if (pool.researchCandidateId != null && pool.researchState != LeaderResearchState.TRIAL_READY) { + logger.warn( + "拒绝从未试跟建议的研究候选创建 Leader 池试跟配置: poolId={}, leaderId={}, researchState={}", + pool.id, + pool.leaderId, + pool.researchState + ) + return Result.failure(LeaderPoolResearchCandidateNotReadyException()) + } if (request.enableImmediately && !request.confirm) { logger.warn("拒绝未确认的立即启用试跟配置: poolId={}, leaderId={}", pool.id, pool.leaderId) return Result.failure(LeaderPoolConfirmRequiredException()) diff --git a/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolServiceTest.kt b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolServiceTest.kt index 19d6952..ea6d4c2 100644 --- a/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolServiceTest.kt +++ b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/leaderpool/LeaderPoolServiceTest.kt @@ -12,6 +12,7 @@ import com.wrbug.polymarketbot.entity.CopyTrading import com.wrbug.polymarketbot.entity.Leader import com.wrbug.polymarketbot.entity.LeaderPool import com.wrbug.polymarketbot.enums.LeaderPoolStatus +import com.wrbug.polymarketbot.enums.LeaderResearchState import com.wrbug.polymarketbot.repository.AccountRepository import com.wrbug.polymarketbot.repository.CopyTradingRepository import com.wrbug.polymarketbot.repository.LeaderPoolRepository @@ -180,6 +181,20 @@ class LeaderPoolServiceTest { assertEquals(LeaderPoolStatus.TRIAL, poolCaptor.value.status) } + @Test + fun `research pool item must be trial ready before creating trial config`() { + Mockito.`when`(leaderPoolRepository.findById(10L)).thenReturn( + Optional.of(pool(researchCandidateId = 99, researchState = LeaderResearchState.PAPER)) + ) + + val result = service.createTrialConfig(LeaderPoolCreateTrialConfigRequest(poolId = 10, accountId = 2)) + + assertTrue(result.isFailure) + assertTrue(result.exceptionOrNull() is LeaderPoolResearchCandidateNotReadyException) + Mockito.verify(accountRepository, Mockito.never()).findById(2L) + Mockito.verify(copyTradingService, Mockito.never()).createCopyTrading(anyCreateRequest()) + } + @Test fun `create trial failure leaves pool status unchanged`() { Mockito.`when`(leaderPoolRepository.findById(10L)).thenReturn(Optional.of(pool())) @@ -261,11 +276,15 @@ class LeaderPoolServiceTest { private fun pool( id: Long = 10, leaderId: Long = 1, - status: LeaderPoolStatus = LeaderPoolStatus.CANDIDATE + status: LeaderPoolStatus = LeaderPoolStatus.CANDIDATE, + researchCandidateId: Long? = null, + researchState: LeaderResearchState? = null ) = LeaderPool( id = id, leaderId = leaderId, status = status, + researchCandidateId = researchCandidateId, + researchState = researchState, suggestedFixedAmount = BigDecimal("1"), suggestedMaxDailyOrders = 10, suggestedMaxDailyLoss = BigDecimal("5"), diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index 749a875..956c550 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -619,6 +619,7 @@ "createTrial": "Create Trial Config", "trialConfirmTitle": "This creates a disabled FIXED small trial config", "trialConfirmDesc": "Confirm fixed amount, max daily orders, max daily loss, price range, and max position before submitting.", + "trialRequiresResearchReady": "Research candidates must be trial-ready before a trial config can be created.", "account": "Account", "selectAccount": "Please select an account", "noAccounts": "No accounts yet", diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 7c036e6..47e899c 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -619,6 +619,7 @@ "createTrial": "创建试跟配置", "trialConfirmTitle": "将创建默认禁用的小额 FIXED 跟单配置", "trialConfirmDesc": "提交前请确认固定金额、每日最大单数、每日最大亏损、价格区间和最大持仓。", + "trialRequiresResearchReady": "研究候选进入建议试跟后才能创建试跟配置", "account": "账户", "selectAccount": "请选择账户", "noAccounts": "暂无账户,请先添加账户", diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index 13bcd76..00a8fd6 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -619,6 +619,7 @@ "createTrial": "創建試跟配置", "trialConfirmTitle": "將創建默認禁用的小額 FIXED 跟單配置", "trialConfirmDesc": "提交前請確認固定金額、每日最大單數、每日最大虧損、價格區間和最大持倉。", + "trialRequiresResearchReady": "研究候選進入建議試跟後才能創建試跟配置", "account": "賬戶", "selectAccount": "請選擇賬戶", "noAccounts": "暫無賬戶,請先添加賬戶", diff --git a/frontend/src/pages/LeaderPool.tsx b/frontend/src/pages/LeaderPool.tsx index a5dd020..e108275 100644 --- a/frontend/src/pages/LeaderPool.tsx +++ b/frontend/src/pages/LeaderPool.tsx @@ -19,6 +19,7 @@ import { Statistic, Table, Tag, + Tooltip, Typography, message } from 'antd' @@ -45,6 +46,9 @@ const formatDate = (timestamp?: number) => { return dayjs(timestamp).format('YYYY-MM-DD HH:mm') } +const canCreateTrialConfig = (item: LeaderPoolItem) => + !item.researchCandidateId || item.researchState === 'TRIAL_READY' + const LeaderPool: React.FC = () => { const { t } = useTranslation() const navigate = useNavigate() @@ -333,36 +337,46 @@ const LeaderPool: React.FC = () => { key: 'actions', fixed: 'right' as const, width: 320, - render: (_: unknown, item: LeaderPoolItem) => ( - - - - + render: (_: unknown, item: LeaderPoolItem) => { + const trialAllowed = canCreateTrialConfig(item) + const trialButton = ( - handleRemove(item)} - > - - - - ) + ) + return ( + + + + + {trialAllowed ? trialButton : ( + + {trialButton} + + )} + handleRemove(item)} + > + + + + ) + } } ]