diff --git a/store/strategy.go b/store/strategy.go
index 19bc9526..8860e8fe 100644
--- a/store/strategy.go
+++ b/store/strategy.go
@@ -444,6 +444,9 @@ func (s *StrategyStore) Delete(userID, id string) error {
if st.IsDefault {
return fmt.Errorf("cannot delete system default strategy")
}
+ if st.IsActive {
+ return fmt.Errorf("cannot delete active strategy")
+ }
}
// Check if any trader references this strategy
diff --git a/web/src/components/trader/ModelConfigModal.tsx b/web/src/components/trader/ModelConfigModal.tsx
index 4b8d0250..e24c5054 100644
--- a/web/src/components/trader/ModelConfigModal.tsx
+++ b/web/src/components/trader/ModelConfigModal.tsx
@@ -77,8 +77,11 @@ export function ModelConfigModal({
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
- if (!selectedModelId || !apiKey.trim()) return
- onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined)
+ if (!selectedModelId) return
+ const key = apiKey.trim()
+ // Allow empty key when editing an existing model (backend preserves existing key)
+ if (!key && !editingModelId) return
+ onSave(selectedModelId, key, baseUrl.trim() || undefined, modelName.trim() || undefined)
}
const availableModels = allModels || []
@@ -833,9 +836,9 @@ function Claw402ConfigForm({
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 48a6ef8c..57bc63ee 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1066,6 +1066,7 @@ export const translations = {
newStrategyName: 'New Strategy',
strategyCopy: 'Strategy Copy',
strategyDeleted: 'Strategy deleted',
+ cannotDeleteActiveStrategy: 'Active strategy cannot be deleted',
confirmDeleteStrategy: 'Delete this strategy?',
confirmDelete: 'Confirm Delete',
delete: 'Delete',
@@ -2373,6 +2374,7 @@ export const translations = {
newStrategyName: '新策略',
strategyCopy: '策略副本',
strategyDeleted: '策略已删除',
+ cannotDeleteActiveStrategy: '激活中的策略不能删除',
confirmDeleteStrategy: '确定删除此策略?',
confirmDelete: '确认删除',
delete: '删除',
@@ -3482,6 +3484,7 @@ export const translations = {
newStrategyName: 'Strategi Baru',
strategyCopy: 'Salinan Strategi',
strategyDeleted: 'Strategi dihapus',
+ cannotDeleteActiveStrategy: 'Strategi aktif tidak bisa dihapus',
confirmDeleteStrategy: 'Hapus strategi ini?',
confirmDelete: 'Konfirmasi Hapus',
delete: 'Hapus',
diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx
index 5e4ee255..62ddea00 100644
--- a/web/src/pages/StrategyStudioPage.tsx
+++ b/web/src/pages/StrategyStudioPage.tsx
@@ -30,7 +30,7 @@ import {
Upload,
Globe,
} from 'lucide-react'
-import type { Strategy, StrategyConfig, AIModel } from '../types'
+import type { Strategy, StrategyConfig, AIModel, GridStrategyConfig } from '../types'
import { confirmToast, notify } from '../lib/notify'
import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor'
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
@@ -94,6 +94,7 @@ export function StrategyStudioPage() {
duration_ms?: number
} | null>(null)
const [isRunningAiTest, setIsRunningAiTest] = useState(false)
+ const gridConfigCacheRef = useRef>({})
const toggleSection = (section: keyof typeof expandedSections) => {
setExpandedSections((prev) => ({
@@ -156,6 +157,12 @@ export function StrategyStudioPage() {
fetchAiModels()
}, [fetchStrategies, fetchAiModels])
+ useEffect(() => {
+ if (!selectedStrategy?.id || !editingConfig?.grid_config) return
+
+ gridConfigCacheRef.current[selectedStrategy.id] = { ...editingConfig.grid_config }
+ }, [selectedStrategy?.id, editingConfig?.grid_config])
+
// Track previous language to detect actual changes
const prevLanguageRef = useRef(language)
@@ -248,6 +255,12 @@ export function StrategyStudioPage() {
// Delete strategy
const handleDeleteStrategy = async (id: string) => {
if (!token) return
+ const strategy = strategies.find((item) => item.id === id)
+
+ if (strategy?.is_active) {
+ notify.error(tr('cannotDeleteActiveStrategy'))
+ return
+ }
// Check if strategy is in use by any trader before showing dialog
try {
@@ -443,14 +456,51 @@ export function StrategyStudioPage() {
section: K,
value: StrategyConfig[K]
) => {
- if (!editingConfig) return
- setEditingConfig({
- ...editingConfig,
- [section]: value,
+ setEditingConfig((prev) => {
+ if (!prev) return prev
+ return {
+ ...prev,
+ [section]: value,
+ }
})
setHasChanges(true)
}
+ const handleStrategyTypeChange = (strategyType: NonNullable) => {
+ if (selectedStrategy?.is_default) return
+
+ const cachedGridConfig = selectedStrategy?.id
+ ? gridConfigCacheRef.current[selectedStrategy.id]
+ : null
+
+ setEditingConfig((prev) => {
+ if (!prev) return prev
+
+ if (strategyType === 'ai_trading') {
+ if (selectedStrategy?.id && prev.grid_config) {
+ gridConfigCacheRef.current[selectedStrategy.id] = { ...prev.grid_config }
+ }
+
+ return {
+ ...prev,
+ strategy_type: 'ai_trading',
+ // Use null so the field is preserved in JSON and backend merge can actually clear it.
+ grid_config: null,
+ }
+ }
+
+ return {
+ ...prev,
+ strategy_type: 'grid_trading',
+ grid_config: cachedGridConfig ?? prev.grid_config ?? { ...defaultGridConfig },
+ }
+ })
+
+ setPromptPreview(null)
+ setAiTestResult(null)
+ setHasChanges(true)
+ }
+
// Fetch prompt preview
const fetchPromptPreview = async () => {
if (!token || !editingConfig) return
@@ -666,7 +716,7 @@ export function StrategyStudioPage() {
-
{tr('strategyStudio')}
+
{tr('title')}
{tr('subtitle')}
@@ -743,8 +793,9 @@ export function StrategyStudioPage() {
@@ -848,13 +899,7 @@ export function StrategyStudioPage() {