mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 01:14:40 +08:00
feat: add strategy publish settings and reorder navigation
- Add is_public and config_visible fields to Strategy type - Add PublishSettingsEditor component for strategy studio - Enable GORM AutoMigrate to add new columns - Reorder nav: Market → Config → Dashboard → Strategy → Leaderboard → Arena → Backtest → FAQ - Rename Live to Leaderboard, Debate Arena to Arena
This commit is contained in:
@@ -172,14 +172,7 @@ func NewStrategyStore(db *gorm.DB) *StrategyStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *StrategyStore) initTables() error {
|
func (s *StrategyStore) initTables() error {
|
||||||
// For PostgreSQL with existing table, skip AutoMigrate
|
// AutoMigrate will add missing columns without dropping existing data
|
||||||
if s.db.Dialector.Name() == "postgres" {
|
|
||||||
var tableExists int64
|
|
||||||
s.db.Raw(`SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'strategies'`).Scan(&tableExists)
|
|
||||||
if tableExists > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s.db.AutoMigrate(&Strategy{})
|
return s.db.AutoMigrate(&Strategy{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ export default function HeaderBar({
|
|||||||
{(() => {
|
{(() => {
|
||||||
// Define all navigation tabs
|
// Define all navigation tabs
|
||||||
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
|
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
|
||||||
{ page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true },
|
|
||||||
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : 'Market', requiresAuth: true },
|
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : 'Market', requiresAuth: true },
|
||||||
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
||||||
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
|
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
|
||||||
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
|
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
|
||||||
|
{ page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true },
|
||||||
{ page: 'debate', path: '/debate', label: t('debateNav', language), requiresAuth: true },
|
{ page: 'debate', path: '/debate', label: t('debateNav', language), requiresAuth: true },
|
||||||
{ page: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true },
|
{ page: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true },
|
||||||
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
|
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
|
||||||
@@ -449,11 +449,11 @@ export default function HeaderBar({
|
|||||||
{/* Mobile Navigation Tabs - Show all tabs */}
|
{/* Mobile Navigation Tabs - Show all tabs */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
|
const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [
|
||||||
{ page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true },
|
|
||||||
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : 'Market', requiresAuth: true },
|
{ page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : 'Market', requiresAuth: true },
|
||||||
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
{ page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
||||||
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
|
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true },
|
||||||
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
|
{ page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true },
|
||||||
|
{ page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true },
|
||||||
{ page: 'debate', path: '/debate', label: t('debateNav', language), requiresAuth: true },
|
{ page: 'debate', path: '/debate', label: t('debateNav', language), requiresAuth: true },
|
||||||
{ page: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true },
|
{ page: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true },
|
||||||
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
|
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
|
||||||
|
|||||||
197
web/src/components/strategy/PublishSettingsEditor.tsx
Normal file
197
web/src/components/strategy/PublishSettingsEditor.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import { Globe, Lock, Eye, EyeOff } from 'lucide-react'
|
||||||
|
|
||||||
|
interface PublishSettingsEditorProps {
|
||||||
|
isPublic: boolean
|
||||||
|
configVisible: boolean
|
||||||
|
onIsPublicChange: (value: boolean) => void
|
||||||
|
onConfigVisibleChange: (value: boolean) => void
|
||||||
|
disabled?: boolean
|
||||||
|
language: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PublishSettingsEditor({
|
||||||
|
isPublic,
|
||||||
|
configVisible,
|
||||||
|
onIsPublicChange,
|
||||||
|
onConfigVisibleChange,
|
||||||
|
disabled = false,
|
||||||
|
language,
|
||||||
|
}: PublishSettingsEditorProps) {
|
||||||
|
const t = (key: string) => {
|
||||||
|
const translations: Record<string, Record<string, string>> = {
|
||||||
|
publishToMarket: { zh: '发布到策略市场', en: 'Publish to Market' },
|
||||||
|
publishDesc: { zh: '策略将在市场公开展示,其他用户可发现并使用', en: 'Strategy will be publicly visible in the marketplace' },
|
||||||
|
showConfig: { zh: '公开配置参数', en: 'Show Config' },
|
||||||
|
showConfigDesc: { zh: '允许他人查看和复制详细配置', en: 'Allow others to view and clone config details' },
|
||||||
|
private: { zh: '私有', en: 'PRIVATE' },
|
||||||
|
public: { zh: '公开', en: 'PUBLIC' },
|
||||||
|
hidden: { zh: '隐藏', en: 'HIDDEN' },
|
||||||
|
visible: { zh: '可见', en: 'VISIBLE' },
|
||||||
|
}
|
||||||
|
return translations[key]?.[language] || key
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 发布开关 */}
|
||||||
|
<div
|
||||||
|
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
|
style={{
|
||||||
|
background: isPublic
|
||||||
|
? 'linear-gradient(135deg, rgba(14, 203, 129, 0.15) 0%, rgba(14, 203, 129, 0.05) 100%)'
|
||||||
|
: 'linear-gradient(135deg, #1E2329 0%, #0B0E11 100%)',
|
||||||
|
border: isPublic ? '1px solid rgba(14, 203, 129, 0.4)' : '1px solid #2B3139',
|
||||||
|
boxShadow: isPublic ? '0 0 20px rgba(14, 203, 129, 0.1)' : 'none',
|
||||||
|
}}
|
||||||
|
onClick={() => !disabled && onIsPublicChange(!isPublic)}
|
||||||
|
>
|
||||||
|
{/* Top glow line */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 w-full h-[1px] transition-opacity duration-300"
|
||||||
|
style={{
|
||||||
|
background: isPublic
|
||||||
|
? 'linear-gradient(90deg, transparent, #0ECB81, transparent)'
|
||||||
|
: 'linear-gradient(90deg, transparent, #2B3139, transparent)',
|
||||||
|
opacity: isPublic ? 1 : 0.5
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="p-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="p-2.5 rounded-lg transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
background: isPublic ? 'rgba(14, 203, 129, 0.2)' : '#0B0E11',
|
||||||
|
border: isPublic ? '1px solid rgba(14, 203, 129, 0.3)' : '1px solid #2B3139'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPublic ? (
|
||||||
|
<Globe className="w-5 h-5" style={{ color: '#0ECB81' }} />
|
||||||
|
) : (
|
||||||
|
<Lock className="w-5 h-5" style={{ color: '#848E9C' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('publishToMarket')}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
||||||
|
{t('publishDesc')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle with status */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-mono font-bold tracking-wider"
|
||||||
|
style={{ color: isPublic ? '#0ECB81' : '#848E9C' }}
|
||||||
|
>
|
||||||
|
{isPublic ? t('public') : t('private')}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="relative w-12 h-6 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
background: isPublic
|
||||||
|
? 'linear-gradient(90deg, #0ECB81, #4ade80)'
|
||||||
|
: '#2B3139',
|
||||||
|
boxShadow: isPublic ? '0 0 10px rgba(14, 203, 129, 0.4)' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute top-1 w-4 h-4 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
background: '#EAECEF',
|
||||||
|
left: isPublic ? '28px' : '4px',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 配置可见性开关 - 仅在公开时显示 */}
|
||||||
|
{isPublic && (
|
||||||
|
<div
|
||||||
|
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
|
style={{
|
||||||
|
background: configVisible
|
||||||
|
? 'linear-gradient(135deg, rgba(168, 85, 247, 0.15) 0%, rgba(168, 85, 247, 0.05) 100%)'
|
||||||
|
: 'linear-gradient(135deg, #1E2329 0%, #0B0E11 100%)',
|
||||||
|
border: configVisible ? '1px solid rgba(168, 85, 247, 0.4)' : '1px solid #2B3139',
|
||||||
|
boxShadow: configVisible ? '0 0 20px rgba(168, 85, 247, 0.1)' : 'none',
|
||||||
|
}}
|
||||||
|
onClick={() => !disabled && onConfigVisibleChange(!configVisible)}
|
||||||
|
>
|
||||||
|
{/* Top glow line */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 w-full h-[1px] transition-opacity duration-300"
|
||||||
|
style={{
|
||||||
|
background: configVisible
|
||||||
|
? 'linear-gradient(90deg, transparent, #a855f7, transparent)'
|
||||||
|
: 'linear-gradient(90deg, transparent, #2B3139, transparent)',
|
||||||
|
opacity: configVisible ? 1 : 0.5
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="p-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="p-2.5 rounded-lg transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
background: configVisible ? 'rgba(168, 85, 247, 0.2)' : '#0B0E11',
|
||||||
|
border: configVisible ? '1px solid rgba(168, 85, 247, 0.3)' : '1px solid #2B3139'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{configVisible ? (
|
||||||
|
<Eye className="w-5 h-5" style={{ color: '#a855f7' }} />
|
||||||
|
) : (
|
||||||
|
<EyeOff className="w-5 h-5" style={{ color: '#848E9C' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('showConfig')}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
||||||
|
{t('showConfigDesc')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle with status */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-mono font-bold tracking-wider"
|
||||||
|
style={{ color: configVisible ? '#a855f7' : '#848E9C' }}
|
||||||
|
>
|
||||||
|
{configVisible ? t('visible') : t('hidden')}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="relative w-12 h-6 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
background: configVisible
|
||||||
|
? 'linear-gradient(90deg, #a855f7, #c084fc)'
|
||||||
|
: '#2B3139',
|
||||||
|
boxShadow: configVisible ? '0 0 10px rgba(168, 85, 247, 0.4)' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute top-1 w-4 h-4 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
background: '#EAECEF',
|
||||||
|
left: configVisible ? '28px' : '4px',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublishSettingsEditor
|
||||||
@@ -18,11 +18,11 @@ export const translations = {
|
|||||||
view: 'View',
|
view: 'View',
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
realtimeNav: 'Live',
|
realtimeNav: 'Leaderboard',
|
||||||
configNav: 'Config',
|
configNav: 'Config',
|
||||||
dashboardNav: 'Dashboard',
|
dashboardNav: 'Dashboard',
|
||||||
strategyNav: 'Strategy',
|
strategyNav: 'Strategy',
|
||||||
debateNav: 'Debate Arena',
|
debateNav: 'Arena',
|
||||||
faqNav: 'FAQ',
|
faqNav: 'FAQ',
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
@@ -1226,11 +1226,11 @@ export const translations = {
|
|||||||
view: '查看',
|
view: '查看',
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
realtimeNav: '实时',
|
realtimeNav: '排行榜',
|
||||||
configNav: '配置',
|
configNav: '配置',
|
||||||
dashboardNav: '看板',
|
dashboardNav: '看板',
|
||||||
strategyNav: '策略',
|
strategyNav: '策略',
|
||||||
debateNav: '行情辩论',
|
debateNav: '竞技场',
|
||||||
faqNav: '常见问题',
|
faqNav: '常见问题',
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
Send,
|
Send,
|
||||||
Download,
|
Download,
|
||||||
Upload,
|
Upload,
|
||||||
|
Globe,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { Strategy, StrategyConfig, AIModel } from '../types'
|
import type { Strategy, StrategyConfig, AIModel } from '../types'
|
||||||
import { confirmToast, notify } from '../lib/notify'
|
import { confirmToast, notify } from '../lib/notify'
|
||||||
@@ -35,6 +36,7 @@ import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor'
|
|||||||
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
||||||
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
|
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
|
||||||
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
|
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
|
||||||
|
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ export function StrategyStudioPage() {
|
|||||||
riskControl: false,
|
riskControl: false,
|
||||||
promptSections: false,
|
promptSections: false,
|
||||||
customPrompt: false,
|
customPrompt: false,
|
||||||
|
publishSettings: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Right panel states
|
// Right panel states
|
||||||
@@ -181,6 +184,8 @@ export function StrategyStudioPage() {
|
|||||||
description: '',
|
description: '',
|
||||||
is_active: false,
|
is_active: false,
|
||||||
is_default: false,
|
is_default: false,
|
||||||
|
is_public: false,
|
||||||
|
config_visible: true,
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@@ -343,11 +348,14 @@ export function StrategyStudioPage() {
|
|||||||
name: selectedStrategy.name,
|
name: selectedStrategy.name,
|
||||||
description: selectedStrategy.description,
|
description: selectedStrategy.description,
|
||||||
config: editingConfig,
|
config: editingConfig,
|
||||||
|
is_public: selectedStrategy.is_public,
|
||||||
|
config_visible: selectedStrategy.config_visible,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!response.ok) throw new Error('Failed to save strategy')
|
if (!response.ok) throw new Error('Failed to save strategy')
|
||||||
setHasChanges(false)
|
setHasChanges(false)
|
||||||
|
notify.success(language === 'zh' ? '策略已保存' : 'Strategy saved')
|
||||||
await fetchStrategies()
|
await fetchStrategies()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||||
@@ -462,6 +470,7 @@ export function StrategyStudioPage() {
|
|||||||
duration: { zh: '耗时', en: 'Duration' },
|
duration: { zh: '耗时', en: 'Duration' },
|
||||||
noModel: { zh: '请先配置 AI 模型', en: 'Please configure AI model first' },
|
noModel: { zh: '请先配置 AI 模型', en: 'Please configure AI model first' },
|
||||||
testNote: { zh: '使用真实 AI 模型测试,不执行交易', en: 'Test with real AI, no trading' },
|
testNote: { zh: '使用真实 AI 模型测试,不执行交易', en: 'Test with real AI, no trading' },
|
||||||
|
publishSettings: { zh: '发布设置', en: 'Publish' },
|
||||||
}
|
}
|
||||||
return translations[key]?.[language] || key
|
return translations[key]?.[language] || key
|
||||||
}
|
}
|
||||||
@@ -557,6 +566,28 @@ export function StrategyStudioPage() {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'publishSettings' as const,
|
||||||
|
icon: Globe,
|
||||||
|
color: '#0ECB81',
|
||||||
|
title: t('publishSettings'),
|
||||||
|
content: selectedStrategy && (
|
||||||
|
<PublishSettingsEditor
|
||||||
|
isPublic={selectedStrategy.is_public ?? false}
|
||||||
|
configVisible={selectedStrategy.config_visible ?? true}
|
||||||
|
onIsPublicChange={(value) => {
|
||||||
|
setSelectedStrategy({ ...selectedStrategy, is_public: value })
|
||||||
|
setHasChanges(true)
|
||||||
|
}}
|
||||||
|
onConfigVisibleChange={(value) => {
|
||||||
|
setSelectedStrategy({ ...selectedStrategy, config_visible: value })
|
||||||
|
setHasChanges(true)
|
||||||
|
}}
|
||||||
|
disabled={selectedStrategy?.is_default}
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -658,7 +689,7 @@ export function StrategyStudioPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 mt-1">
|
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
||||||
{strategy.is_active && (
|
{strategy.is_active && (
|
||||||
<span className="px-1.5 py-0.5 text-[10px] rounded" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
|
<span className="px-1.5 py-0.5 text-[10px] rounded" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
|
||||||
{t('active')}
|
{t('active')}
|
||||||
@@ -669,6 +700,12 @@ export function StrategyStudioPage() {
|
|||||||
{t('default')}
|
{t('default')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{strategy.is_public && (
|
||||||
|
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5" style={{ background: 'rgba(96, 165, 250, 0.15)', color: '#60a5fa' }}>
|
||||||
|
<Globe className="w-2.5 h-2.5" />
|
||||||
|
{language === 'zh' ? '公开' : 'Public'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -428,11 +428,32 @@ export interface Strategy {
|
|||||||
description: string;
|
description: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
|
is_public: boolean; // 是否在策略市场公开
|
||||||
|
config_visible: boolean; // 配置参数是否公开可见
|
||||||
config: StrategyConfig;
|
config: StrategyConfig;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 策略使用统计
|
||||||
|
export interface StrategyStats {
|
||||||
|
clone_count: number; // 被克隆次数
|
||||||
|
active_users: number; // 当前使用人数
|
||||||
|
top_performers?: StrategyPerformer[]; // 收益排行
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略使用者收益排行
|
||||||
|
export interface StrategyPerformer {
|
||||||
|
user_id: string;
|
||||||
|
user_name: string; // 脱敏后的用户名
|
||||||
|
total_pnl_pct: number; // 总收益率
|
||||||
|
total_pnl: number; // 总收益金额
|
||||||
|
win_rate: number; // 胜率
|
||||||
|
trade_count: number; // 交易次数
|
||||||
|
using_since: string; // 使用开始时间
|
||||||
|
rank: number; // 排名
|
||||||
|
}
|
||||||
|
|
||||||
export interface PromptSectionsConfig {
|
export interface PromptSectionsConfig {
|
||||||
role_definition?: string;
|
role_definition?: string;
|
||||||
trading_frequency?: string;
|
trading_frequency?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user