diff --git a/store/strategy.go b/store/strategy.go index 993fb79e..d7c62063 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -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{}) } diff --git a/web/src/components/HeaderBar.tsx b/web/src/components/HeaderBar.tsx index 77c9b032..874dc237 100644 --- a/web/src/components/HeaderBar.tsx +++ b/web/src/components/HeaderBar.tsx @@ -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 }, diff --git a/web/src/components/strategy/PublishSettingsEditor.tsx b/web/src/components/strategy/PublishSettingsEditor.tsx new file mode 100644 index 00000000..3176d479 --- /dev/null +++ b/web/src/components/strategy/PublishSettingsEditor.tsx @@ -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> = { + 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 ( +
+ {/* 发布开关 */} +
!disabled && onIsPublicChange(!isPublic)} + > + {/* Top glow line */} +
+ +
+
+
+ {isPublic ? ( + + ) : ( + + )} +
+
+
+ {t('publishToMarket')} +
+
+ {t('publishDesc')} +
+
+
+ + {/* Toggle with status */} +
+ + {isPublic ? t('public') : t('private')} + +
+
+
+
+
+
+ + {/* 配置可见性开关 - 仅在公开时显示 */} + {isPublic && ( +
!disabled && onConfigVisibleChange(!configVisible)} + > + {/* Top glow line */} +
+ +
+
+
+ {configVisible ? ( + + ) : ( + + )} +
+
+
+ {t('showConfig')} +
+
+ {t('showConfigDesc')} +
+
+
+ + {/* Toggle with status */} +
+ + {configVisible ? t('visible') : t('hidden')} + +
+
+
+
+
+
+ )} +
+ ) +} + +export default PublishSettingsEditor diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index e02e01ec..f30e2622 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -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 diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx index ec4ba30f..53d918d8 100644 --- a/web/src/pages/StrategyStudioPage.tsx +++ b/web/src/pages/StrategyStudioPage.tsx @@ -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() {
), }, + { + key: 'publishSettings' as const, + icon: Globe, + color: '#0ECB81', + title: t('publishSettings'), + content: selectedStrategy && ( + { + 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() { )}
-
+
{strategy.is_active && ( {t('active')} @@ -669,6 +700,12 @@ export function StrategyStudioPage() { {t('default')} )} + {strategy.is_public && ( + + + {language === 'zh' ? '公开' : 'Public'} + + )}
))} diff --git a/web/src/types.ts b/web/src/types.ts index 046e40ad..23142291 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -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;