Split strategy config by strategy type

This commit is contained in:
lky-spec
2026-04-28 20:19:24 +08:00
parent 2d45e7ab15
commit 25d0b30ea9
11 changed files with 502 additions and 148 deletions

View File

@@ -12,6 +12,17 @@ function getShortName(fullName: string): string {
return parts.length > 1 ? parts[parts.length - 1] : fullName
}
function getStrategyAIConfig(strategy: Strategy) {
return strategy.config.ai_config || (
strategy.config.coin_source && strategy.config.risk_control
? {
coin_source: strategy.config.coin_source,
risk_control: strategy.config.risk_control,
}
: null
)
}
// 交易所注册链接配置
const EXCHANGE_REGISTRATION_LINKS: Record<string, { url: string; hasReferral?: boolean }> = {
binance: { url: 'https://www.binance.com/join?ref=NOFXENG', hasReferral: true },
@@ -314,16 +325,27 @@ export function TraderConfigModal({
<p className="text-sm text-[#848E9C] mb-2">
{selectedStrategy.description || (language === 'zh' ? '无描述' : 'No description')}
</p>
<div className="grid grid-cols-2 gap-2 text-xs text-[#848E9C]">
<div>
{t('coinSource', language)}: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' :
selectedStrategy.config.coin_source.source_type === 'ai500' ? 'AI500' :
selectedStrategy.config.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
{selectedStrategy.config.strategy_type === 'grid_trading' && selectedStrategy.config.grid_config ? (
<div className="grid grid-cols-2 gap-2 text-xs text-[#848E9C]">
<div>{language === 'zh' ? '交易对' : 'Symbol'}: {selectedStrategy.config.grid_config.symbol || '-'}</div>
<div>{language === 'zh' ? '网格数' : 'Grids'}: {selectedStrategy.config.grid_config.grid_count}</div>
</div>
<div>
{t('marginLimit', language)}: {((selectedStrategy.config.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%
</div>
</div>
) : (() => {
const aiConfig = getStrategyAIConfig(selectedStrategy)
if (!aiConfig) return null
return (
<div className="grid grid-cols-2 gap-2 text-xs text-[#848E9C]">
<div>
{t('coinSource', language)}: {aiConfig.coin_source.source_type === 'static' ? '固定币种' :
aiConfig.coin_source.source_type === 'ai500' ? 'AI500' :
aiConfig.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
</div>
<div>
{t('marginLimit', language)}: {((aiConfig.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%
</div>
</div>
)
})()}
</div>
)}
</div>

View File

@@ -179,16 +179,17 @@ export function StrategyMarketPage() {
}
const getIndicatorList = (config: any) => {
if (!config?.indicators) return []
const indicatorsConfig = config?.ai_config?.indicators || config?.indicators
if (!indicatorsConfig) return []
const indicators = []
if (config.indicators.enable_ema) indicators.push('EMA')
if (config.indicators.enable_macd) indicators.push('MACD')
if (config.indicators.enable_rsi) indicators.push('RSI')
if (config.indicators.enable_atr) indicators.push('ATR')
if (config.indicators.enable_boll) indicators.push('BOLL')
if (config.indicators.enable_volume) indicators.push('VOL')
if (config.indicators.enable_oi) indicators.push('OI')
if (config.indicators.enable_funding_rate) indicators.push('FR')
if (indicatorsConfig.enable_ema) indicators.push('EMA')
if (indicatorsConfig.enable_macd) indicators.push('MACD')
if (indicatorsConfig.enable_rsi) indicators.push('RSI')
if (indicatorsConfig.enable_atr) indicators.push('ATR')
if (indicatorsConfig.enable_boll) indicators.push('BOLL')
if (indicatorsConfig.enable_volume) indicators.push('VOL')
if (indicatorsConfig.enable_oi) indicators.push('OI')
if (indicatorsConfig.enable_funding_rate) indicators.push('FR')
return indicators
}
@@ -439,7 +440,7 @@ export function StrategyMarketPage() {
</div>
{/* Risk Control */}
{strategy.config.risk_control && (
{(strategy.config.ai_config?.risk_control || strategy.config.risk_control) && (
<div className="flex justify-between items-center text-[10px]">
<div className="flex gap-3">
<div className="flex flex-col">
@@ -447,7 +448,7 @@ export function StrategyMarketPage() {
LEV
</span>
<span className="text-zinc-300 font-bold">
{strategy.config.risk_control
{(strategy.config.ai_config?.risk_control || strategy.config.risk_control)
.btc_eth_max_leverage || '-'}
x
</span>
@@ -457,7 +458,7 @@ export function StrategyMarketPage() {
POS
</span>
<span className="text-zinc-300 font-bold">
{strategy.config.risk_control
{(strategy.config.ai_config?.risk_control || strategy.config.risk_control)
.max_positions || '-'}
</span>
</div>

View File

@@ -33,6 +33,7 @@ import {
import type {
Strategy,
StrategyConfig,
AIStrategyConfig,
AIModel,
GridStrategyConfig,
} from '../types'
@@ -52,6 +53,32 @@ import { t } from '../i18n/translations'
const API_BASE = import.meta.env.VITE_API_BASE || ''
const getAIConfig = (config: StrategyConfig): AIStrategyConfig | null => {
if (config.ai_config) return config.ai_config
if (config.coin_source && config.indicators && config.risk_control) {
return {
coin_source: config.coin_source,
indicators: config.indicators,
risk_control: config.risk_control,
prompt_sections: config.prompt_sections,
custom_prompt: config.custom_prompt,
}
}
return null
}
const normalizeStrategyConfig = (config: StrategyConfig): StrategyConfig => {
const aiConfig = getAIConfig(config)
const strategyType = config.strategy_type || 'ai_trading'
return {
strategy_type: strategyType,
language: config.language,
ai_config: aiConfig || undefined,
grid_config: config.grid_config,
publish_config: config.publish_config,
}
}
export function StrategyStudioPage() {
const { token } = useAuth()
const { language } = useLanguage()
@@ -164,7 +191,7 @@ export function StrategyStudioPage() {
selectedStrategyIDRef.current = nextSelected?.id || ''
if (!hasChangesRef.current || !preservedSelection) {
setEditingConfig(nextSelected?.config || null)
setEditingConfig(nextSelected?.config ? normalizeStrategyConfig(nextSelected.config) : null)
}
if (!nextSelected) {
setEditingConfig(null)
@@ -234,7 +261,7 @@ export function StrategyStudioPage() {
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!response.ok) return
const defaultConfig = await response.json()
const defaultConfig = normalizeStrategyConfig(await response.json())
// Update only the prompt sections and language field
setEditingConfig((prev) => {
@@ -242,7 +269,12 @@ export function StrategyStudioPage() {
return {
...prev,
language: language as 'zh' | 'en',
prompt_sections: defaultConfig.prompt_sections,
ai_config: prev.ai_config
? {
...prev.ai_config,
prompt_sections: defaultConfig.ai_config?.prompt_sections,
}
: prev.ai_config,
}
})
setHasChanges(true)
@@ -263,7 +295,7 @@ export function StrategyStudioPage() {
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!configResponse.ok) throw new Error('Failed to fetch default config')
const defaultConfig = await configResponse.json()
const defaultConfig = normalizeStrategyConfig(await configResponse.json())
const response = await fetch(`${API_BASE}/api/strategies`, {
method: 'POST',
@@ -479,7 +511,7 @@ export function StrategyStudioPage() {
try {
// Always sync the config language with the current interface language
const configWithLanguage = {
...editingConfig,
...normalizeStrategyConfig(editingConfig),
language: language as 'zh' | 'en',
}
const response = await fetch(
@@ -525,6 +557,23 @@ export function StrategyStudioPage() {
setHasChanges(true)
}
const updateAIConfig = <K extends keyof AIStrategyConfig>(
section: K,
value: AIStrategyConfig[K]
) => {
setEditingConfig((prev) => {
if (!prev || !prev.ai_config) return prev
return {
...prev,
ai_config: {
...prev.ai_config,
[section]: value,
},
}
})
setHasChanges(true)
}
const handleStrategyTypeChange = (
strategyType: NonNullable<StrategyConfig['strategy_type']>
) => {
@@ -547,6 +596,7 @@ export function StrategyStudioPage() {
return {
...prev,
strategy_type: 'ai_trading',
ai_config: getAIConfig(prev) || prev.ai_config,
// Use null so the field is preserved in JSON and backend merge can actually clear it.
grid_config: null,
}
@@ -555,6 +605,7 @@ export function StrategyStudioPage() {
return {
...prev,
strategy_type: 'grid_trading',
ai_config: undefined,
grid_config: cachedGridConfig ??
prev.grid_config ?? { ...defaultGridConfig },
}
@@ -643,6 +694,7 @@ export function StrategyStudioPage() {
// Get current strategy type (default to ai_trading if not set)
const currentStrategyType = editingConfig?.strategy_type || 'ai_trading'
const currentAIConfig = editingConfig ? getAIConfig(editingConfig) : null
const configSections = [
// Grid Config - only for grid_trading
@@ -668,10 +720,10 @@ export function StrategyStudioPage() {
color: '#F0B90B',
title: tr('coinSource'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
content: currentAIConfig && (
<CoinSourceEditor
config={editingConfig.coin_source}
onChange={(coinSource) => updateConfig('coin_source', coinSource)}
config={currentAIConfig.coin_source}
onChange={(coinSource) => updateAIConfig('coin_source', coinSource)}
disabled={selectedStrategy?.is_default}
language={language}
/>
@@ -683,10 +735,10 @@ export function StrategyStudioPage() {
color: '#0ECB81',
title: tr('indicators'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
content: currentAIConfig && (
<IndicatorEditor
config={editingConfig.indicators}
onChange={(indicators) => updateConfig('indicators', indicators)}
config={currentAIConfig.indicators}
onChange={(indicators) => updateAIConfig('indicators', indicators)}
disabled={selectedStrategy?.is_default}
language={language}
/>
@@ -698,10 +750,10 @@ export function StrategyStudioPage() {
color: '#F6465D',
title: tr('riskControl'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
content: currentAIConfig && (
<RiskControlEditor
config={editingConfig.risk_control}
onChange={(riskControl) => updateConfig('risk_control', riskControl)}
config={currentAIConfig.risk_control}
onChange={(riskControl) => updateAIConfig('risk_control', riskControl)}
disabled={selectedStrategy?.is_default}
language={language}
/>
@@ -713,11 +765,11 @@ export function StrategyStudioPage() {
color: '#a855f7',
title: tr('promptSections'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
content: currentAIConfig && (
<PromptSectionsEditor
config={editingConfig.prompt_sections}
config={currentAIConfig.prompt_sections}
onChange={(promptSections) =>
updateConfig('prompt_sections', promptSections)
updateAIConfig('prompt_sections', promptSections)
}
disabled={selectedStrategy?.is_default}
language={language}
@@ -730,14 +782,14 @@ export function StrategyStudioPage() {
color: '#60a5fa',
title: tr('customPrompt'),
forStrategyType: 'ai_trading' as const,
content: editingConfig && (
content: currentAIConfig && (
<div>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{tr('customPromptDesc')}
</p>
<textarea
value={editingConfig.custom_prompt || ''}
onChange={(e) => updateConfig('custom_prompt', e.target.value)}
value={currentAIConfig.custom_prompt || ''}
onChange={(e) => updateAIConfig('custom_prompt', e.target.value)}
disabled={selectedStrategy?.is_default}
placeholder={tr('customPromptPlaceholder')}
className="w-full h-32 px-3 py-2 rounded-lg resize-none font-mono text-xs"
@@ -848,7 +900,7 @@ export function StrategyStudioPage() {
key={strategy.id}
onClick={() => {
setSelectedStrategy(strategy)
setEditingConfig(strategy.config)
setEditingConfig(normalizeStrategyConfig(strategy.config))
setHasChanges(false)
setPromptPreview(null)
setAiTestResult(null)

View File

@@ -44,13 +44,30 @@ export interface StrategyConfig {
// Language setting: "zh" for Chinese, "en" for English
// Determines the language used for data formatting and prompt generation
language?: 'zh' | 'en';
// AI trading configuration. Legacy flat fields below are accepted only for
// old data returned before the schema was split by strategy type.
ai_config?: AIStrategyConfig;
coin_source?: CoinSourceConfig;
indicators?: IndicatorConfig;
custom_prompt?: string;
risk_control?: RiskControlConfig;
prompt_sections?: PromptSectionsConfig;
// Grid trading configuration (only used when strategy_type is 'grid_trading')
grid_config?: GridStrategyConfig | null;
publish_config?: PublishStrategyConfig;
}
export interface AIStrategyConfig {
coin_source: CoinSourceConfig;
indicators: IndicatorConfig;
custom_prompt?: string;
risk_control: RiskControlConfig;
prompt_sections?: PromptSectionsConfig;
// Grid trading configuration (only used when strategy_type is 'grid_trading')
grid_config?: GridStrategyConfig | null;
}
export interface PublishStrategyConfig {
is_public: boolean;
config_visible: boolean;
}
// Grid trading specific configuration