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) => (
-
- } onClick={() => window.open(item.profileUrl, '_blank', 'noopener,noreferrer')}>
- Profile
-
-
-
+ render: (_: unknown, item: LeaderPoolItem) => {
+ const trialAllowed = canCreateTrialConfig(item)
+ const trialButton = (
- handleRemove(item)}
- >
-
-
-
- )
+ )
+ return (
+
+ } onClick={() => window.open(item.profileUrl, '_blank', 'noopener,noreferrer')}>
+ Profile
+
+
+
+ {trialAllowed ? trialButton : (
+
+ {trialButton}
+
+ )}
+ handleRemove(item)}
+ >
+
+
+
+ )
+ }
}
]