mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +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:
+1
-8
@@ -172,14 +172,7 @@ func NewStrategyStore(db *gorm.DB) *StrategyStore {
|
||||
}
|
||||
|
||||
func (s *StrategyStore) initTables() error {
|
||||
// For PostgreSQL with existing table, skip AutoMigrate
|
||||
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
|
||||
}
|
||||
}
|
||||
// AutoMigrate will add missing columns without dropping existing data
|
||||
return s.db.AutoMigrate(&Strategy{})
|
||||
}
|
||||
|
||||
|
||||
@@ -101,11 +101,11 @@ export default function HeaderBar({
|
||||
{(() => {
|
||||
// Define all navigation tabs
|
||||
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: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
||||
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', 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: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true },
|
||||
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
|
||||
@@ -449,11 +449,11 @@ export default function HeaderBar({
|
||||
{/* Mobile Navigation Tabs - Show all tabs */}
|
||||
{(() => {
|
||||
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: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true },
|
||||
{ page: 'trader', path: '/dashboard', label: t('dashboardNav', 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: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true },
|
||||
{ page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false },
|
||||
|
||||
@@ -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',
|
||||
|
||||
// Navigation
|
||||
realtimeNav: 'Live',
|
||||
realtimeNav: 'Leaderboard',
|
||||
configNav: 'Config',
|
||||
dashboardNav: 'Dashboard',
|
||||
strategyNav: 'Strategy',
|
||||
debateNav: 'Debate Arena',
|
||||
debateNav: 'Arena',
|
||||
faqNav: 'FAQ',
|
||||
|
||||
// Footer
|
||||
@@ -1226,11 +1226,11 @@ export const translations = {
|
||||
view: '查看',
|
||||
|
||||
// Navigation
|
||||
realtimeNav: '实时',
|
||||
realtimeNav: '排行榜',
|
||||
configNav: '配置',
|
||||
dashboardNav: '看板',
|
||||
strategyNav: '策略',
|
||||
debateNav: '行情辩论',
|
||||
debateNav: '竞技场',
|
||||
faqNav: '常见问题',
|
||||
|
||||
// Footer
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Send,
|
||||
Download,
|
||||
Upload,
|
||||
Globe,
|
||||
} from 'lucide-react'
|
||||
import type { Strategy, StrategyConfig, AIModel } from '../types'
|
||||
import { confirmToast, notify } from '../lib/notify'
|
||||
@@ -35,6 +36,7 @@ import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor'
|
||||
import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
||||
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
|
||||
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
|
||||
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
||||
|
||||
@@ -61,6 +63,7 @@ export function StrategyStudioPage() {
|
||||
riskControl: false,
|
||||
promptSections: false,
|
||||
customPrompt: false,
|
||||
publishSettings: false,
|
||||
})
|
||||
|
||||
// Right panel states
|
||||
@@ -181,6 +184,8 @@ export function StrategyStudioPage() {
|
||||
description: '',
|
||||
is_active: false,
|
||||
is_default: false,
|
||||
is_public: false,
|
||||
config_visible: true,
|
||||
config: defaultConfig,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
@@ -343,11 +348,14 @@ export function StrategyStudioPage() {
|
||||
name: selectedStrategy.name,
|
||||
description: selectedStrategy.description,
|
||||
config: editingConfig,
|
||||
is_public: selectedStrategy.is_public,
|
||||
config_visible: selectedStrategy.config_visible,
|
||||
}),
|
||||
}
|
||||
)
|
||||
if (!response.ok) throw new Error('Failed to save strategy')
|
||||
setHasChanges(false)
|
||||
notify.success(language === 'zh' ? '策略已保存' : 'Strategy saved')
|
||||
await fetchStrategies()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
@@ -462,6 +470,7 @@ export function StrategyStudioPage() {
|
||||
duration: { zh: '耗时', en: 'Duration' },
|
||||
noModel: { zh: '请先配置 AI 模型', en: 'Please configure AI model first' },
|
||||
testNote: { zh: '使用真实 AI 模型测试,不执行交易', en: 'Test with real AI, no trading' },
|
||||
publishSettings: { zh: '发布设置', en: 'Publish' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
@@ -557,6 +566,28 @@ export function StrategyStudioPage() {
|
||||
</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 (
|
||||
@@ -658,7 +689,7 @@ export function StrategyStudioPage() {
|
||||
)}
|
||||
</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 && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
|
||||
{t('active')}
|
||||
@@ -669,6 +700,12 @@ export function StrategyStudioPage() {
|
||||
{t('default')}
|
||||
</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>
|
||||
))}
|
||||
|
||||
@@ -428,11 +428,32 @@ export interface Strategy {
|
||||
description: string;
|
||||
is_active: boolean;
|
||||
is_default: boolean;
|
||||
is_public: boolean; // 是否在策略市场公开
|
||||
config_visible: boolean; // 配置参数是否公开可见
|
||||
config: StrategyConfig;
|
||||
created_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 {
|
||||
role_definition?: string;
|
||||
trading_frequency?: string;
|
||||
|
||||
Reference in New Issue
Block a user