mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 22:36:58 +08:00
Split strategy config by strategy type
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user