mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 01:14:40 +08:00
feat(store): prevent deletion of active strategies and update translations (#1461)
Co-authored-by: Dean <afei.wuhao@gmail.com>
This commit is contained in:
@@ -444,6 +444,9 @@ func (s *StrategyStore) Delete(userID, id string) error {
|
|||||||
if st.IsDefault {
|
if st.IsDefault {
|
||||||
return fmt.Errorf("cannot delete system default strategy")
|
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
|
// Check if any trader references this strategy
|
||||||
|
|||||||
@@ -1066,6 +1066,7 @@ export const translations = {
|
|||||||
newStrategyName: 'New Strategy',
|
newStrategyName: 'New Strategy',
|
||||||
strategyCopy: 'Strategy Copy',
|
strategyCopy: 'Strategy Copy',
|
||||||
strategyDeleted: 'Strategy deleted',
|
strategyDeleted: 'Strategy deleted',
|
||||||
|
cannotDeleteActiveStrategy: 'Active strategy cannot be deleted',
|
||||||
confirmDeleteStrategy: 'Delete this strategy?',
|
confirmDeleteStrategy: 'Delete this strategy?',
|
||||||
confirmDelete: 'Confirm Delete',
|
confirmDelete: 'Confirm Delete',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
@@ -2373,6 +2374,7 @@ export const translations = {
|
|||||||
newStrategyName: '新策略',
|
newStrategyName: '新策略',
|
||||||
strategyCopy: '策略副本',
|
strategyCopy: '策略副本',
|
||||||
strategyDeleted: '策略已删除',
|
strategyDeleted: '策略已删除',
|
||||||
|
cannotDeleteActiveStrategy: '激活中的策略不能删除',
|
||||||
confirmDeleteStrategy: '确定删除此策略?',
|
confirmDeleteStrategy: '确定删除此策略?',
|
||||||
confirmDelete: '确认删除',
|
confirmDelete: '确认删除',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
@@ -3482,6 +3484,7 @@ export const translations = {
|
|||||||
newStrategyName: 'Strategi Baru',
|
newStrategyName: 'Strategi Baru',
|
||||||
strategyCopy: 'Salinan Strategi',
|
strategyCopy: 'Salinan Strategi',
|
||||||
strategyDeleted: 'Strategi dihapus',
|
strategyDeleted: 'Strategi dihapus',
|
||||||
|
cannotDeleteActiveStrategy: 'Strategi aktif tidak bisa dihapus',
|
||||||
confirmDeleteStrategy: 'Hapus strategi ini?',
|
confirmDeleteStrategy: 'Hapus strategi ini?',
|
||||||
confirmDelete: 'Konfirmasi Hapus',
|
confirmDelete: 'Konfirmasi Hapus',
|
||||||
delete: 'Hapus',
|
delete: 'Hapus',
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
Globe,
|
Globe,
|
||||||
} from 'lucide-react'
|
} 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 { confirmToast, notify } from '../lib/notify'
|
||||||
import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor'
|
import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor'
|
||||||
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
||||||
@@ -94,6 +94,7 @@ export function StrategyStudioPage() {
|
|||||||
duration_ms?: number
|
duration_ms?: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [isRunningAiTest, setIsRunningAiTest] = useState(false)
|
const [isRunningAiTest, setIsRunningAiTest] = useState(false)
|
||||||
|
const gridConfigCacheRef = useRef<Record<string, GridStrategyConfig>>({})
|
||||||
|
|
||||||
const toggleSection = (section: keyof typeof expandedSections) => {
|
const toggleSection = (section: keyof typeof expandedSections) => {
|
||||||
setExpandedSections((prev) => ({
|
setExpandedSections((prev) => ({
|
||||||
@@ -156,6 +157,12 @@ export function StrategyStudioPage() {
|
|||||||
fetchAiModels()
|
fetchAiModels()
|
||||||
}, [fetchStrategies, 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
|
// Track previous language to detect actual changes
|
||||||
const prevLanguageRef = useRef(language)
|
const prevLanguageRef = useRef(language)
|
||||||
|
|
||||||
@@ -248,6 +255,12 @@ export function StrategyStudioPage() {
|
|||||||
// Delete strategy
|
// Delete strategy
|
||||||
const handleDeleteStrategy = async (id: string) => {
|
const handleDeleteStrategy = async (id: string) => {
|
||||||
if (!token) return
|
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
|
// Check if strategy is in use by any trader before showing dialog
|
||||||
try {
|
try {
|
||||||
@@ -443,14 +456,51 @@ export function StrategyStudioPage() {
|
|||||||
section: K,
|
section: K,
|
||||||
value: StrategyConfig[K]
|
value: StrategyConfig[K]
|
||||||
) => {
|
) => {
|
||||||
if (!editingConfig) return
|
setEditingConfig((prev) => {
|
||||||
setEditingConfig({
|
if (!prev) return prev
|
||||||
...editingConfig,
|
return {
|
||||||
[section]: value,
|
...prev,
|
||||||
|
[section]: value,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
setHasChanges(true)
|
setHasChanges(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStrategyTypeChange = (strategyType: NonNullable<StrategyConfig['strategy_type']>) => {
|
||||||
|
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
|
// Fetch prompt preview
|
||||||
const fetchPromptPreview = async () => {
|
const fetchPromptPreview = async () => {
|
||||||
if (!token || !editingConfig) return
|
if (!token || !editingConfig) return
|
||||||
@@ -666,7 +716,7 @@ export function StrategyStudioPage() {
|
|||||||
<Sparkles className="w-5 h-5 text-black" />
|
<Sparkles className="w-5 h-5 text-black" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-bold text-nofx-text">{tr('strategyStudio')}</h1>
|
<h1 className="text-lg font-bold text-nofx-text">{tr('title')}</h1>
|
||||||
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
|
<p className="text-xs text-nofx-text-muted">{tr('subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -743,8 +793,9 @@ export function StrategyStudioPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||||
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
|
disabled={strategy.is_active}
|
||||||
title={tr('deleteTooltip')}
|
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
|
||||||
|
title={strategy.is_active ? tr('cannotDeleteActiveStrategy') : tr('deleteTooltip')}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -848,13 +899,7 @@ export function StrategyStudioPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => handleStrategyTypeChange('ai_trading')}
|
||||||
if (!selectedStrategy?.is_default) {
|
|
||||||
updateConfig('strategy_type', 'ai_trading')
|
|
||||||
// Clear grid config when switching to AI trading
|
|
||||||
updateConfig('grid_config', undefined)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={selectedStrategy?.is_default}
|
disabled={selectedStrategy?.is_default}
|
||||||
className={`p-3 rounded-lg border transition-all ${
|
className={`p-3 rounded-lg border transition-all ${
|
||||||
(!editingConfig.strategy_type || editingConfig.strategy_type === 'ai_trading')
|
(!editingConfig.strategy_type || editingConfig.strategy_type === 'ai_trading')
|
||||||
@@ -869,15 +914,7 @@ export function StrategyStudioPage() {
|
|||||||
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
|
<p className="text-xs text-nofx-text-muted text-left">{tr('aiTradingDesc')}</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => handleStrategyTypeChange('grid_trading')}
|
||||||
if (!selectedStrategy?.is_default) {
|
|
||||||
updateConfig('strategy_type', 'grid_trading')
|
|
||||||
// Initialize grid config if not exists
|
|
||||||
if (!editingConfig.grid_config) {
|
|
||||||
updateConfig('grid_config', defaultGridConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={selectedStrategy?.is_default}
|
disabled={selectedStrategy?.is_default}
|
||||||
className={`p-3 rounded-lg border transition-all ${
|
className={`p-3 rounded-lg border transition-all ${
|
||||||
editingConfig.strategy_type === 'grid_trading'
|
editingConfig.strategy_type === 'grid_trading'
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export interface StrategyConfig {
|
|||||||
risk_control: RiskControlConfig;
|
risk_control: RiskControlConfig;
|
||||||
prompt_sections?: PromptSectionsConfig;
|
prompt_sections?: PromptSectionsConfig;
|
||||||
// Grid trading configuration (only used when strategy_type is 'grid_trading')
|
// Grid trading configuration (only used when strategy_type is 'grid_trading')
|
||||||
grid_config?: GridStrategyConfig;
|
grid_config?: GridStrategyConfig | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid trading specific configuration
|
// Grid trading specific configuration
|
||||||
|
|||||||
Reference in New Issue
Block a user