From 110bf52908e06f8a6b1b70d8c7f1f85c3658ba09 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Tue, 30 Jun 2026 16:03:52 +0800 Subject: [PATCH] feat: cream terminal redesign, English-only UI, autopilot launch fixes - Redesign dashboard into a cream-paper + vermilion IBM Plex Mono terminal (live L2 order book, cost/liq map, WS K-line, signal matrix, orchestration topology, risk radar, execution log, current positions, equity curve) - Convert all user-facing UI and backend strings/prompts from Chinese to English (multi-language retained, default English) - Add /api/statistics/full endpoint + full-stats frontend wiring - Fix Autopilot launch: reuse the existing trader instead of creating duplicates (eliminates repeat ~35s create cost and stale-trader 404s); launch sends 5m scan interval - Fix unreadable toasts: cream theme with high-contrast text + per-type accent - Silence background dashboard polls (getTraderConfig) to stop error-toast spam --- api/handler_hyperliquid_wallet.go | 2 +- api/handler_stats_full.go | 58 ++ api/handler_trader.go | 134 ++--- api/handler_user.go | 6 +- api/handler_user_default_strategy_test.go | 14 +- api/handler_vergex.go | 22 + api/server.go | 5 + kernel/engine.go | 86 ++- kernel/engine_directional_test.go | 36 ++ kernel/engine_prompt.go | 248 ++++---- kernel/engine_prompt_test.go | 20 +- kernel/formatter.go | 128 ++-- kernel/grid_engine.go | 228 +++---- kernel/prompt_builder.go | 140 ++--- kernel/prompt_builder_test.go | 60 +- kernel/schema.go | 190 +++--- manager/trader_manager.go | 11 +- mcp/payment/claw402.go | 2 +- provider/coinank/coinank_api/kline_test.go | 12 +- provider/hyperliquid/kline_test.go | 50 +- provider/nofxos/coin.go | 16 +- provider/nofxos/netflow.go | 26 +- provider/nofxos/oi.go | 16 +- provider/nofxos/price.go | 16 +- provider/vergex/client.go | 19 + store/strategy.go | 112 ++-- store/strategy_schema_test.go | 6 +- telegram/agent/agent.go | 2 +- telegram/agent/agent_test.go | 42 +- telegram/agent/manager.go | 2 +- telegram/agent/prompt.go | 4 +- telegram/bot.go | 56 +- trader/auto_trader.go | 10 + trader/auto_trader_force.go | 99 +++ trader/auto_trader_force_test.go | 57 ++ trader/auto_trader_loop.go | 3 + trader/auto_trader_throttle.go | 12 +- trader/auto_trader_throttle_test.go | 24 +- trader/hyperliquid/builder_fee_test.go | 2 +- trader/hyperliquid/trader.go | 2 +- web/src/components/auth/LoginPage.tsx | 62 +- .../components/auth/LoginRequiredOverlay.tsx | 19 +- .../auth/OnboardingModeSelector.tsx | 24 +- web/src/components/auth/RegisterPage.test.tsx | 64 +- web/src/components/auth/RegisterPage.tsx | 75 ++- .../auth/RegistrationDisabled.test.tsx | 8 +- .../components/auth/RegistrationDisabled.tsx | 6 +- web/src/components/auth/ResetPasswordPage.tsx | 26 +- web/src/components/charts/AdvancedChart.tsx | 159 +++-- web/src/components/charts/ChartTabs.tsx | 46 +- web/src/components/charts/ChartWithOrders.tsx | 70 +-- .../charts/ChartWithOrdersSimple.tsx | 46 +- web/src/components/charts/ComparisonChart.tsx | 96 ++- web/src/components/charts/EquityChart.tsx | 148 +++-- .../components/charts/TradingViewChart.tsx | 106 ++-- web/src/components/common/Container.tsx | 8 +- .../components/common/DeepVoidBackground.tsx | 25 +- web/src/components/common/ExchangeIcons.tsx | 14 +- web/src/components/common/Header.tsx | 20 +- web/src/components/common/HeaderBar.tsx | 52 +- .../common/HyperliquidWalletConnect.tsx | 168 +++--- .../components/common/LanguageSwitcher.tsx | 8 +- web/src/components/common/MetricTooltip.tsx | 84 +-- web/src/components/common/ModelIcons.tsx | 8 +- web/src/components/common/SiteFooter.tsx | 54 +- .../common/WebCryptoEnvironmentCheck.tsx | 14 +- .../components/common/WhitelistFullPage.tsx | 51 +- web/src/components/faq/FAQContent.tsx | 206 +++---- web/src/components/faq/FAQLayout.tsx | 38 +- web/src/components/faq/FAQSearchBar.tsx | 4 +- web/src/components/faq/FAQSidebar.tsx | 6 +- web/src/components/landing/AboutSection.tsx | 70 +-- .../components/landing/AnimatedSection.tsx | 2 +- .../components/landing/CommunitySection.tsx | 46 +- .../components/landing/FeaturesSection.tsx | 88 ++- web/src/components/landing/FooterSection.tsx | 34 +- web/src/components/landing/HeroSection.tsx | 66 +- .../components/landing/HowItWorksSection.tsx | 60 +- web/src/components/landing/LoginModal.tsx | 18 +- .../landing/brand/AgentTerminal.tsx | 105 ++-- .../landing/brand/BrandFeatures.tsx | 14 +- .../components/landing/brand/BrandHero.tsx | 22 +- .../components/landing/brand/BrandStats.tsx | 12 +- web/src/components/landing/core/AgentGrid.tsx | 59 +- .../components/landing/core/DeploymentHub.tsx | 44 +- web/src/components/landing/core/LiveFeed.tsx | 31 +- .../components/landing/core/TerminalHero.tsx | 80 ++- web/src/components/modals/SetupPage.tsx | 68 +-- .../components/modals/TwoStageKeyModal.tsx | 46 +- web/src/components/strategy/GridRiskPanel.tsx | 136 ++--- web/src/components/terminal/Candles.tsx | 75 +++ web/src/components/terminal/ExecutionLog.tsx | 300 ++++++++++ web/src/components/terminal/FlowMarkets.tsx | 150 +++++ web/src/components/terminal/KlineChart.tsx | 166 +++++ .../components/terminal/LiquidationMap.tsx | 236 ++++++++ .../terminal/OrchestrationTopology.tsx | 213 +++++++ web/src/components/terminal/OrderBook.tsx | 311 ++++++++++ web/src/components/terminal/RiskRadar.tsx | 383 ++++++++++++ web/src/components/terminal/SignalMatrix.tsx | 192 ++++++ .../components/terminal/TerminalDashboard.tsx | 566 ++++++++++++++++++ web/src/components/terminal/terminal.css | 139 +++++ web/src/components/trader/AITradersPage.tsx | 20 +- .../trader/AutopilotLaunchPanel.tsx | 78 ++- .../components/trader/BeginnerGuideCards.tsx | 80 +-- .../trader/CompetitionPage.test.tsx | 26 +- web/src/components/trader/CompetitionPage.tsx | 137 +++-- .../components/trader/ConfigStatusGrid.tsx | 104 ++-- web/src/components/trader/DecisionCard.tsx | 142 +++-- .../components/trader/ExchangeConfigModal.tsx | 196 +++--- web/src/components/trader/ModelCard.tsx | 20 +- .../components/trader/ModelConfigModal.tsx | 356 +++++------ .../components/trader/ModelStepIndicator.tsx | 8 +- web/src/components/trader/PositionHistory.tsx | 205 ++++--- .../components/trader/TelegramConfigModal.tsx | 120 ++-- web/src/components/trader/Tooltip.tsx | 8 +- .../components/trader/TraderConfigModal.tsx | 132 ++-- .../trader/TraderConfigViewModal.tsx | 32 +- .../trader/TraderLaunchGuestPage.tsx | 52 +- web/src/components/trader/TradersList.tsx | 84 +-- web/src/components/trader/utils.ts | 4 +- web/src/components/ui/alert-dialog.tsx | 6 +- web/src/components/ui/input.tsx | 2 +- web/src/components/ui/select.tsx | 6 +- web/src/hooks/useCounterAnimation.ts | 2 +- web/src/index.css | 161 +++-- web/src/lib/api/data.ts | 112 ++++ web/src/lib/api/traders.ts | 10 +- web/src/lib/apiGuard.test.ts | 28 +- web/src/lib/crypto.ts | 28 +- web/src/lib/notify.tsx | 6 +- web/src/lib/text.ts | 15 +- web/src/main.tsx | 8 +- web/src/pages/BeginnerOnboardingPage.tsx | 86 +-- web/src/pages/FAQPage.tsx | 16 +- web/src/pages/LandingPage.tsx | 2 +- web/src/pages/PageNotFound.tsx | 8 +- web/src/pages/SettingsPage.tsx | 84 +-- web/src/pages/StrategyStudioPage.tsx | 268 ++++----- web/src/pages/TraderDashboardPage.tsx | 73 ++- web/src/router/AppRoutes.tsx | 49 +- web/src/stores/tradersConfigStore.ts | 16 +- web/src/stores/tradersModalStore.ts | 8 +- web/src/types/config.ts | 12 +- web/src/types/strategy.ts | 42 +- web/src/types/trading.ts | 28 +- web/src/utils/format.ts | 56 +- web/src/utils/indicators.ts | 38 +- web/src/utils/traderColors.ts | 16 +- web/tailwind.config.js | 31 +- 149 files changed, 6835 insertions(+), 3611 deletions(-) create mode 100644 api/handler_stats_full.go create mode 100644 kernel/engine_directional_test.go create mode 100644 trader/auto_trader_force.go create mode 100644 trader/auto_trader_force_test.go create mode 100644 web/src/components/terminal/Candles.tsx create mode 100644 web/src/components/terminal/ExecutionLog.tsx create mode 100644 web/src/components/terminal/FlowMarkets.tsx create mode 100644 web/src/components/terminal/KlineChart.tsx create mode 100644 web/src/components/terminal/LiquidationMap.tsx create mode 100644 web/src/components/terminal/OrchestrationTopology.tsx create mode 100644 web/src/components/terminal/OrderBook.tsx create mode 100644 web/src/components/terminal/RiskRadar.tsx create mode 100644 web/src/components/terminal/SignalMatrix.tsx create mode 100644 web/src/components/terminal/TerminalDashboard.tsx create mode 100644 web/src/components/terminal/terminal.css diff --git a/api/handler_hyperliquid_wallet.go b/api/handler_hyperliquid_wallet.go index 126a5620..ce5d1b92 100644 --- a/api/handler_hyperliquid_wallet.go +++ b/api/handler_hyperliquid_wallet.go @@ -16,7 +16,7 @@ import ( const ( defaultHyperliquidBuilderAddress = "0x891dc6f05ad47a3c1a05da55e7a7517971faaf0d" - // 0.05% (万5) — matches BuilderInfo.Fee=50 charged at order placement. + // 0.05% (5 bps) — matches BuilderInfo.Fee=50 charged at order placement. // New wallet approvals sign this exact value; existing approvals at the // prior 0.1% cap remain valid because 0.05% is within their approved max. defaultHyperliquidBuilderMaxFee = "0.05%" diff --git a/api/handler_stats_full.go b/api/handler_stats_full.go new file mode 100644 index 00000000..a99aae74 --- /dev/null +++ b/api/handler_stats_full.go @@ -0,0 +1,58 @@ +package api + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// handleStatisticsFull returns the full set of computed performance metrics for +// a single trader: win rate, profit factor, Sharpe ratio, max drawdown, and the +// average win/loss amounts. These are derived from the trader's CLOSED positions +// via store.Position().GetFullStatsByTraderFilters — the same computation the +// strategy engine feeds to the AI, so the dashboard and the model see identical +// numbers. +// +// The existing GET /statistics endpoint only returns cycle/position counts; this +// endpoint exposes the richer trade-quality metrics the terminal dashboard needs. +func (s *Server) handleStatisticsFull(c *gin.Context) { + _, traderID, err := s.getTraderFromQuery(c) + if err != nil { + SafeBadRequest(c, "Invalid trader ID") + return + } + + trader, err := s.traderManager.GetTrader(traderID) + if err != nil { + SafeNotFound(c, "Trader") + return + } + + store := trader.GetStore() + if store == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Store not available"}) + return + } + + // Aggregate across the trader's historical IDs exactly like the position + // history endpoint (handler_order.go). One-click "NOFX Autopilot" relaunches + // create fresh trader rows, but the closed positions stay under the old + // generated IDs (which embed userID + "claw402"). Without this, a freshly + // relaunched Autopilot would report only the current incarnation's trades + // instead of its real lifetime history. + userID := c.GetString("user_id") + traderIDs := []string{trader.GetID()} + var traderIDPatterns []string + if strings.EqualFold(strings.TrimSpace(trader.GetName()), "NOFX Autopilot") && strings.TrimSpace(userID) != "" { + traderIDPatterns = append(traderIDPatterns, "%_"+userID+"_claw402_%") + } + + stats, err := store.Position().GetFullStatsByTraderFilters(traderIDs, traderIDPatterns) + if err != nil { + SafeInternalError(c, "Get full statistics", err) + return + } + + c.JSON(http.StatusOK, stats) +} diff --git a/api/handler_trader.go b/api/handler_trader.go index 552f27b9..ca19d3fc 100644 --- a/api/handler_trader.go +++ b/api/handler_trader.go @@ -61,21 +61,21 @@ type UpdateTraderRequest struct { func formatTraderCreationError(reason, nextStep string) string { if nextStep == "" { - return fmt.Sprintf("这次未能创建机器人:%s。", reason) + return fmt.Sprintf("Failed to create the bot this time: %s.", reason) } - return fmt.Sprintf("这次未能创建机器人:%s。%s。", reason, nextStep) + return fmt.Sprintf("Failed to create the bot this time: %s. %s.", reason, nextStep) } func traderCreationRequestError(reason string) string { - return formatTraderCreationError(reason, "请检查你刚刚填写的内容后,再重新提交") + return formatTraderCreationError(reason, "Please check the information you just entered and submit again") } func validateTraderLeverageRange(btcEthLeverage, altcoinLeverage int) (string, string) { if btcEthLeverage < 0 || btcEthLeverage > maxManualBTCETHLeverage { - return traderCreationRequestError("BTC/ETH 杠杆倍数需要在 1 到 20 倍之间"), "trader.create.invalid_btc_eth_leverage" + return traderCreationRequestError("BTC/ETH leverage must be between 1x and 20x"), "trader.create.invalid_btc_eth_leverage" } if altcoinLeverage < 0 || altcoinLeverage > maxManualAltLeverage { - return traderCreationRequestError("山寨币杠杆倍数需要在 1 到 20 倍之间"), "trader.create.invalid_altcoin_leverage" + return traderCreationRequestError("Altcoin leverage must be between 1x and 20x"), "trader.create.invalid_altcoin_leverage" } return "", "" } @@ -90,15 +90,15 @@ func isSupportedTraderSymbol(symbol string) bool { func exchangeDisplayName(exchange *store.Exchange) string { if exchange == nil { - return "所选交易所账户" + return "the selected exchange account" } if exchange.AccountName != "" { - return fmt.Sprintf("%s(%s)", exchange.Name, exchange.AccountName) + return fmt.Sprintf("%s (%s)", exchange.Name, exchange.AccountName) } if exchange.Name != "" { return exchange.Name } - return "所选交易所账户" + return "the selected exchange account" } func missingExchangeFields(exchange *store.Exchange) []string { @@ -127,10 +127,10 @@ func missingExchangeFields(exchange *store.Exchange) []string { } case "hyperliquid": if exchange.APIKey == "" { - missing = append(missing, "私钥") + missing = append(missing, "Private Key") } if strings.TrimSpace(exchange.HyperliquidWalletAddr) == "" { - missing = append(missing, "钱包地址") + missing = append(missing, "Wallet Address") } case "aster": if strings.TrimSpace(exchange.AsterUser) == "" { @@ -144,7 +144,7 @@ func missingExchangeFields(exchange *store.Exchange) []string { } case "lighter": if strings.TrimSpace(exchange.LighterWalletAddr) == "" { - missing = append(missing, "钱包地址") + missing = append(missing, "Wallet Address") } if exchange.LighterAPIKeyPrivateKey == "" { missing = append(missing, "API Key Private Key") @@ -168,21 +168,21 @@ func mapStringPairs(kv ...string) map[string]string { func validateExchangeForTraderCreation(exchange *store.Exchange) (string, string, map[string]string) { if exchange == nil { - return formatTraderCreationError("还没有找到你选择的交易所账户", "请前往「设置 > 交易所配置」先添加一个可用账户,再回来创建机器人"), + return formatTraderCreationError("The exchange account you selected was not found", "Please go to \"Settings > Exchange Config\" and add an available account first, then come back to create the bot"), "trader.create.exchange_not_found", nil } if !exchange.Enabled { return formatTraderCreationError( - fmt.Sprintf("交易所账户「%s」目前处于未启用状态", exchangeDisplayName(exchange)), - "请前往「设置 > 交易所配置」启用该账户后,再重新创建机器人", + fmt.Sprintf("Exchange account \"%s\" is currently disabled", exchangeDisplayName(exchange)), + "Please go to \"Settings > Exchange Config\" to enable this account, then create the bot again", ), "trader.create.exchange_disabled", mapStringPairs("exchange_name", exchangeDisplayName(exchange)) } missing := missingExchangeFields(exchange) if len(missing) > 0 { return formatTraderCreationError( - fmt.Sprintf("交易所账户「%s」的配置还不完整,缺少 %s", exchangeDisplayName(exchange), strings.Join(missing, "、")), - "请前往「设置 > 交易所配置」补全该账户的必填信息后,再重新创建机器人", + fmt.Sprintf("The configuration for exchange account \"%s\" is incomplete, missing %s", exchangeDisplayName(exchange), strings.Join(missing, ", ")), + "Please go to \"Settings > Exchange Config\" to complete the required information for this account, then create the bot again", ), "trader.create.exchange_missing_fields", mapStringPairs( "exchange_name", exchangeDisplayName(exchange), "missing_fields", strings.Join(missing, ", "), @@ -194,8 +194,8 @@ func validateExchangeForTraderCreation(exchange *store.Exchange) (string, string return "", "", nil default: return formatTraderCreationError( - fmt.Sprintf("交易所账户「%s」使用了当前版本暂不支持的类型 %s", exchangeDisplayName(exchange), exchange.ExchangeType), - "请改用当前版本支持的交易所账户后,再重新创建机器人", + fmt.Sprintf("Exchange account \"%s\" uses type %s, which is not supported in the current version", exchangeDisplayName(exchange), exchange.ExchangeType), + "Please switch to an exchange account supported by the current version, then create the bot again", ), "trader.create.exchange_unsupported", mapStringPairs( "exchange_name", exchangeDisplayName(exchange), "exchange_type", exchange.ExchangeType, @@ -214,28 +214,28 @@ func classifyTraderSetupReason(reason string) (string, string) { switch { case strings.Contains(lower, "failed to parse strategy config"), strings.Contains(lower, "failed to parse strategy configuration"): - return "trader.reason.strategy_config_invalid", "当前策略配置内容已损坏,系统暂时无法解析" + return "trader.reason.strategy_config_invalid", "The current strategy configuration is corrupted and the system cannot parse it for now" case strings.Contains(lower, "has no strategy configured"): - return "trader.reason.strategy_missing", "当前机器人缺少有效的交易策略配置" + return "trader.reason.strategy_missing", "The current bot is missing a valid trading strategy configuration" case strings.Contains(lower, "failed to parse private key"), (strings.Contains(lower, "invalid hex character") && strings.Contains(lower, "private key")): - return "trader.reason.private_key_invalid", "私钥格式不正确,系统无法识别" + return "trader.reason.private_key_invalid", "The private key format is incorrect and the system cannot recognize it" case strings.Contains(lower, "failed to initialize hyperliquid trader"): - return "trader.reason.hyperliquid_init_failed", "Hyperliquid 账户初始化失败,请确认私钥、主钱包地址和 Agent Wallet 配置是否正确" + return "trader.reason.hyperliquid_init_failed", "Hyperliquid account initialization failed; please confirm the private key, main wallet address, and Agent Wallet configuration are correct" case strings.Contains(lower, "failed to initialize aster trader"): - return "trader.reason.aster_init_failed", "Aster 账户初始化失败,请确认 Aster User、Signer 和私钥是否正确" + return "trader.reason.aster_init_failed", "Aster account initialization failed; please confirm the Aster User, Signer, and private key are correct" case strings.Contains(lower, "failed to get meta information"): - return "trader.reason.exchange_meta_unavailable", "系统暂时无法从交易所读取账户元信息" + return "trader.reason.exchange_meta_unavailable", "The system cannot read account meta information from the exchange for now" case strings.Contains(lower, "security check failed") && strings.Contains(lower, "agent wallet balance too high"): - return "trader.reason.hyperliquid_agent_balance_too_high", "Hyperliquid Agent Wallet 余额过高,不符合当前安全要求" + return "trader.reason.hyperliquid_agent_balance_too_high", "The Hyperliquid Agent Wallet balance is too high and does not meet the current security requirements" case strings.Contains(lower, "failed to initialize account"): - return "trader.reason.exchange_account_init_failed", "交易所账户初始化失败,请确认钱包地址和 API Key 是否匹配" + return "trader.reason.exchange_account_init_failed", "Exchange account initialization failed; please confirm the wallet address and API Key match" case strings.Contains(lower, "unsupported trading platform"): - return "trader.reason.exchange_unsupported", "当前交易所类型暂不支持机器人初始化" + return "trader.reason.exchange_unsupported", "The current exchange type does not support bot initialization" case strings.Contains(lower, "initial balance not set and unable to fetch balance from exchange"): - return "trader.reason.exchange_balance_unavailable", "系统暂时无法从交易所读取账户余额" + return "trader.reason.exchange_balance_unavailable", "The system cannot read the account balance from the exchange for now" case strings.Contains(lower, "timeout"), strings.Contains(lower, "no such host"), strings.Contains(lower, "connection refused"): - return "trader.reason.exchange_service_unreachable", "系统暂时无法连接交易所服务" + return "trader.reason.exchange_service_unreachable", "The system cannot connect to the exchange service for now" default: return "trader.reason.unknown", trimmed } @@ -270,54 +270,54 @@ func traderSetupReasonParams(err error, fallback string, kv ...string) map[strin func describeTraderLoadError(traderName string, err error) string { if err == nil { - return formatTraderCreationError("机器人配置虽然保存了,但运行实例没有成功初始化", "请检查模型、策略和交易所配置是否完整,然后再试一次") + return formatTraderCreationError("The bot configuration was saved, but the runtime instance failed to initialize", "Please check that the model, strategy, and exchange configuration are complete, then try again") } reason := humanizeTraderSetupReason(SanitizeError(err, "")) if reason == "" { return formatTraderCreationError( - fmt.Sprintf("机器人「%s」在初始化运行实例时没有成功启动", traderName), - "请检查模型、策略和交易所配置是否完整,然后再试一次", + fmt.Sprintf("Bot \"%s\" failed to start when initializing its runtime instance", traderName), + "Please check that the model, strategy, and exchange configuration are complete, then try again", ) } return formatTraderCreationError( - fmt.Sprintf("机器人「%s」在初始化运行实例时没有成功启动,原因是:%s", traderName, reason), - "请检查模型、策略和交易所配置是否完整,然后再试一次", + fmt.Sprintf("Bot \"%s\" failed to start when initializing its runtime instance, because: %s", traderName, reason), + "Please check that the model, strategy, and exchange configuration are complete, then try again", ) } func describeTraderCreationWarning(traderName string, err error) string { if err == nil { - return fmt.Sprintf("机器人「%s」已经保存,但当前还没有通过启动前校验。请先检查模型、策略和交易所配置,修正后再点击启动。", traderName) + return fmt.Sprintf("Bot \"%s\" has been saved, but it has not yet passed the pre-start validation. Please check the model, strategy, and exchange configuration first, then click start after fixing them.", traderName) } reason := humanizeTraderSetupReason(SanitizeError(err, "")) if reason == "" { - return fmt.Sprintf("机器人「%s」已经保存,但当前暂时还不能启动。请先检查模型、策略和交易所配置,修正后再点击启动。", traderName) + return fmt.Sprintf("Bot \"%s\" has been saved, but it cannot start for now. Please check the model, strategy, and exchange configuration first, then click start after fixing them.", traderName) } - return fmt.Sprintf("机器人「%s」已经保存,但当前暂时还不能启动,原因是:%s。请先检查模型、策略和交易所配置,修正后再点击启动。", traderName, reason) + return fmt.Sprintf("Bot \"%s\" has been saved, but it cannot start for now, because: %s. Please check the model, strategy, and exchange configuration first, then click start after fixing them.", traderName, reason) } func describeTraderStartError(traderName string, err error) string { if err == nil { - return fmt.Sprintf("这次未能启动机器人:机器人「%s」暂时还不能启动。请检查模型、策略和交易所配置后,再重新点击启动。", traderName) + return fmt.Sprintf("Failed to start the bot this time: bot \"%s\" cannot start for now. Please check the model, strategy, and exchange configuration, then click start again.", traderName) } reason := humanizeTraderSetupReason(SanitizeError(err, "")) if reason == "" { - return fmt.Sprintf("这次未能启动机器人:机器人「%s」暂时还不能启动。请检查模型、策略和交易所配置后,再重新点击启动。", traderName) + return fmt.Sprintf("Failed to start the bot this time: bot \"%s\" cannot start for now. Please check the model, strategy, and exchange configuration, then click start again.", traderName) } - return fmt.Sprintf("这次未能启动机器人:机器人「%s」暂时还不能启动,原因是:%s。请检查模型、策略和交易所配置后,再重新点击启动。", traderName, reason) + return fmt.Sprintf("Failed to start the bot this time: bot \"%s\" cannot start for now, because: %s. Please check the model, strategy, and exchange configuration, then click start again.", traderName, reason) } func formatTraderStartError(reason, nextStep string) string { if nextStep == "" { - return fmt.Sprintf("这次未能启动机器人:%s。", reason) + return fmt.Sprintf("Failed to start the bot this time: %s.", reason) } - return fmt.Sprintf("这次未能启动机器人:%s。%s。", reason, nextStep) + return fmt.Sprintf("Failed to start the bot this time: %s. %s.", reason, nextStep) } // handleCreateTrader Create new AI trader @@ -325,7 +325,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { userID := c.GetString("user_id") var req CreateTraderRequest if err := c.ShouldBindJSON(&req); err != nil { - SafeBadRequestWithDetails(c, traderCreationRequestError("提交的信息不完整,或者格式不正确"), "trader.create.invalid_request", nil) + SafeBadRequestWithDetails(c, traderCreationRequestError("The submitted information is incomplete or has an invalid format"), "trader.create.invalid_request", nil) return } @@ -344,7 +344,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { symbol = strings.TrimSpace(symbol) if !isSupportedTraderSymbol(symbol) { SafeBadRequestWithDetails(c, traderCreationRequestError( - fmt.Sprintf("交易对 %s 的格式不正确,目前只支持 USDT 合约或 Hyperliquid XYZ USDC 标的(SYMBOL-USDC)", symbol), + fmt.Sprintf("The trading pair %s has an invalid format; only USDT perpetuals or Hyperliquid XYZ USDC instruments (SYMBOL-USDC) are currently supported", symbol), ), "trader.create.invalid_symbol", mapStringPairs("symbol", symbol)) return } @@ -354,32 +354,32 @@ func (s *Server) handleCreateTrader(c *gin.Context) { model, err := s.store.AIModel().Get(userID, req.AIModelID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - SafeBadRequestWithDetails(c, formatTraderCreationError("还没有找到你选择的 AI 模型", "请前往「设置 > 模型配置」先添加并启用一个可用模型,再回来创建机器人"), "trader.create.model_not_found", nil) + SafeBadRequestWithDetails(c, formatTraderCreationError("The AI model you selected was not found", "Please go to \"Settings > Model Config\" to add and enable an available model first, then come back to create the bot"), "trader.create.model_not_found", nil) return } SafeError(c, http.StatusInternalServerError, - formatTraderCreationError("暂时无法读取你的 AI 模型配置", "请稍后重试;如果问题持续,再检查本地服务是否正常"), + formatTraderCreationError("Unable to read your AI model configuration for now", "Please retry later; if the problem persists, check whether the local service is running normally"), err, ) return } if !model.Enabled { SafeBadRequestWithDetails(c, formatTraderCreationError( - fmt.Sprintf("AI 模型「%s」目前还没有启用", model.Name), - "请前往「设置 > 模型配置」启用它后,再重新创建机器人", + fmt.Sprintf("AI model \"%s\" is not enabled yet", model.Name), + "Please go to \"Settings > Model Config\" to enable it, then create the bot again", ), "trader.create.model_disabled", mapStringPairs("model_name", model.Name)) return } if model.APIKey == "" { SafeBadRequestWithDetails(c, formatTraderCreationError( - fmt.Sprintf("AI 模型「%s」缺少 API Key 或支付凭证", model.Name), - "请前往「设置 > 模型配置」补全模型凭证后,再重新创建机器人", + fmt.Sprintf("AI model \"%s\" is missing an API Key or payment credentials", model.Name), + "Please go to \"Settings > Model Config\" to complete the model credentials, then create the bot again", ), "trader.create.model_missing_credentials", mapStringPairs("model_name", model.Name)) return } if req.StrategyID == "" { - SafeBadRequestWithDetails(c, formatTraderCreationError("你还没有选择交易策略", "请先选择一个策略,再继续创建机器人"), "trader.create.strategy_required", nil) + SafeBadRequestWithDetails(c, formatTraderCreationError("You have not selected a trading strategy yet", "Please select a strategy first, then continue creating the bot"), "trader.create.strategy_required", nil) return } @@ -387,11 +387,11 @@ func (s *Server) handleCreateTrader(c *gin.Context) { _, err = s.store.Strategy().Get(userID, req.StrategyID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - SafeBadRequestWithDetails(c, formatTraderCreationError("你选择的策略不存在,或者已经被删除了", "请重新选择一个可用策略后,再继续创建机器人"), "trader.create.strategy_not_found", nil) + SafeBadRequestWithDetails(c, formatTraderCreationError("The strategy you selected does not exist or has been deleted", "Please select another available strategy, then continue creating the bot"), "trader.create.strategy_not_found", nil) return } SafeError(c, http.StatusInternalServerError, - formatTraderCreationError("暂时无法读取你选择的策略配置", "请稍后重试;如果问题持续,再检查本地服务是否正常"), + formatTraderCreationError("Unable to read the strategy configuration you selected for now", "Please retry later; if the problem persists, check whether the local service is running normally"), err, ) return @@ -445,7 +445,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { exchanges, err := s.store.Exchange().List(userID) if err != nil { SafeError(c, http.StatusInternalServerError, - formatTraderCreationError("暂时无法读取你的交易所配置", "请稍后重试;如果问题持续,再检查本地服务是否正常"), + formatTraderCreationError("Unable to read your exchange configuration for now", "Please retry later; if the problem persists, check whether the local service is running normally"), err, ) return @@ -469,9 +469,9 @@ func (s *Server) handleCreateTrader(c *gin.Context) { tempTrader, createErr := buildExchangeProbeTrader(exchangeCfg, userID) if createErr != nil { SafeBadRequestWithDetails(c, formatTraderCreationError( - fmt.Sprintf("交易所账户「%s」没有通过初始化校验,原因是:%s", exchangeDisplayName(exchangeCfg), humanizeTraderSetupReason(SanitizeError(createErr, "配置校验未通过"))), - "请前往「设置 > 交易所配置」检查这个账户的密钥、地址和账户信息是否填写正确", - ), "trader.create.exchange_probe_failed", traderSetupReasonParams(createErr, "配置校验未通过", + fmt.Sprintf("Exchange account \"%s\" did not pass initialization validation, because: %s", exchangeDisplayName(exchangeCfg), humanizeTraderSetupReason(SanitizeError(createErr, "Configuration validation failed"))), + "Please go to \"Settings > Exchange Config\" to check whether this account's keys, address, and account information are entered correctly", + ), "trader.create.exchange_probe_failed", traderSetupReasonParams(createErr, "Configuration validation failed", "exchange_name", exchangeDisplayName(exchangeCfg), )) return @@ -521,9 +521,9 @@ func (s *Server) handleCreateTrader(c *gin.Context) { err = s.store.Trader().Create(traderRecord) if err != nil { logger.Infof("❌ Failed to create trader: %v", err) - publicMsg := SanitizeError(err, formatTraderCreationError("机器人配置没有保存成功", "请检查名称、模型、策略和交易所配置后,再试一次")) + publicMsg := SanitizeError(err, formatTraderCreationError("The bot configuration was not saved successfully", "Please check the name, model, strategy, and exchange configuration, then try again")) statusCode := http.StatusBadRequest - if publicMsg == formatTraderCreationError("机器人配置没有保存成功", "请检查名称、模型、策略和交易所配置后,再试一次") { + if publicMsg == formatTraderCreationError("The bot configuration was not saved successfully", "Please check the name, model, strategy, and exchange configuration, then try again") { statusCode = http.StatusInternalServerError } SafeError(c, statusCode, publicMsg, err) @@ -781,8 +781,8 @@ func (s *Server) handleStartTrader(c *gin.Context) { if fullCfg != nil && fullCfg.Exchange != nil && fullCfg.Exchange.ExchangeType == "hyperliquid" && !fullCfg.Exchange.HyperliquidBuilderApproved { SafeBadRequestWithDetails(c, formatTraderStartError( - fmt.Sprintf("机器人「%s」的 Hyperliquid 交易授权尚未完成", traderName), - "请重新连接 Hyperliquid 钱包并完成交易授权后,再启动机器人", + fmt.Sprintf("The Hyperliquid trading authorization for bot \"%s\" is not yet complete", traderName), + "Please reconnect the Hyperliquid wallet and complete the trading authorization, then start the bot", ), "trader.start.hyperliquid_builder_not_approved", mapStringPairs("trader_name", traderName, "exchange_name", exchangeDisplayName(fullCfg.Exchange))) return } @@ -818,25 +818,25 @@ func (s *Server) handleStartTrader(c *gin.Context) { } // Check AI model if fullCfg.AIModel == nil { - SafeBadRequestWithDetails(c, formatTraderStartError("这个机器人关联的 AI 模型不存在", "请前往「设置 > 模型配置」检查后,再重新点击启动"), "trader.start.model_not_found", mapStringPairs("trader_name", traderName)) + SafeBadRequestWithDetails(c, formatTraderStartError("The AI model associated with this bot does not exist", "Please go to \"Settings > Model Config\" to check, then click start again"), "trader.start.model_not_found", mapStringPairs("trader_name", traderName)) return } if !fullCfg.AIModel.Enabled { SafeBadRequestWithDetails(c, formatTraderStartError( - fmt.Sprintf("机器人「%s」关联的 AI 模型「%s」目前还没有启用", traderName, fullCfg.AIModel.Name), - "请前往「设置 > 模型配置」启用它后,再重新点击启动", + fmt.Sprintf("The AI model \"%s\" associated with bot \"%s\" is not enabled yet", fullCfg.AIModel.Name, traderName), + "Please go to \"Settings > Model Config\" to enable it, then click start again", ), "trader.start.model_disabled", mapStringPairs("trader_name", traderName, "model_name", fullCfg.AIModel.Name)) return } // Check exchange if fullCfg.Exchange == nil { - SafeBadRequestWithDetails(c, formatTraderStartError("这个机器人关联的交易所账户不存在", "请前往「设置 > 交易所配置」检查后,再重新点击启动"), "trader.start.exchange_not_found", mapStringPairs("trader_name", traderName)) + SafeBadRequestWithDetails(c, formatTraderStartError("The exchange account associated with this bot does not exist", "Please go to \"Settings > Exchange Config\" to check, then click start again"), "trader.start.exchange_not_found", mapStringPairs("trader_name", traderName)) return } if !fullCfg.Exchange.Enabled { SafeBadRequestWithDetails(c, formatTraderStartError( - fmt.Sprintf("机器人「%s」关联的交易所账户「%s」目前还没有启用", traderName, exchangeDisplayName(fullCfg.Exchange)), - "请前往「设置 > 交易所配置」启用它后,再重新点击启动", + fmt.Sprintf("The exchange account \"%s\" associated with bot \"%s\" is not enabled yet", exchangeDisplayName(fullCfg.Exchange), traderName), + "Please go to \"Settings > Exchange Config\" to enable it, then click start again", ), "trader.start.exchange_disabled", mapStringPairs("trader_name", traderName, "exchange_name", exchangeDisplayName(fullCfg.Exchange))) return } diff --git a/api/handler_user.go b/api/handler_user.go index 029df453..bfe1d30d 100644 --- a/api/handler_user.go +++ b/api/handler_user.go @@ -229,7 +229,7 @@ func (s *Server) createDefaultStrategies(userID string, lang string) error { } locales := map[string]strategyLocale{ "zh": { - defaultStrategy: strategyI18n{"NOFX Claw402 自动策略", "唯一内置策略:每轮读取 Claw402.ai 榜单,逐个拉取 Signal Lab 与成本/清算热力图,再结合原始 K 线决策。"}, + defaultStrategy: strategyI18n{"NOFX Claw402 Auto Strategy", "The only built-in strategy: read the Claw402.ai board each cycle, fetch Signal Lab and cost/liquidation heatmap per candidate, then decide with raw candles."}, }, "en": { defaultStrategy: strategyI18n{"NOFX Claw402 Auto Strategy", "The only built-in strategy: read the Claw402.ai board each cycle, fetch Signal Lab and cost/liquidation heatmap per candidate, then decide with raw candles."}, @@ -318,8 +318,8 @@ func (s *Server) createDefaultStrategies(userID string, lang string) error { } legacyDefaultNames := []string{ - "均衡策略", "稳健策略", "积极策略", - "美股趋势策略", "美股稳健策略", "美股突破策略", + "Balanced Strategy", "Steady Strategy", "Aggressive Strategy", + "US Stock Trend Strategy", "US Stock Steady Strategy", "US Stock Breakout Strategy", "Balanced Strategy", "Conservative Strategy", "Aggressive Strategy", "US Stock Trend Strategy", "US Stock Steady Strategy", "US Stock Breakout Strategy", "Strategi Seimbang", "Strategi Konservatif", "Strategi Agresif", diff --git a/api/handler_user_default_strategy_test.go b/api/handler_user_default_strategy_test.go index fd2c9e04..752f7abd 100644 --- a/api/handler_user_default_strategy_test.go +++ b/api/handler_user_default_strategy_test.go @@ -35,7 +35,7 @@ func TestCreateDefaultStrategiesUsesOneReadyToRunClaw402Preset(t *testing.T) { if strategy.IsActive { activeCount++ } - if strategy.Name == "均衡策略" || strategy.Name == "稳健策略" || strategy.Name == "积极策略" { + if strategy.Name == "Balanced Strategy" || strategy.Name == "Steady Strategy" || strategy.Name == "Aggressive Strategy" { t.Fatalf("legacy crypto-style default strategy still present: %s", strategy.Name) } } @@ -43,9 +43,9 @@ func TestCreateDefaultStrategiesUsesOneReadyToRunClaw402Preset(t *testing.T) { t.Fatalf("expected exactly one active strategy, got %d", activeCount) } - defaultStrategy := byName["NOFX Claw402 自动策略"] + defaultStrategy := byName["NOFX Claw402 Auto Strategy"] if defaultStrategy == nil || !defaultStrategy.IsActive { - t.Fatalf("NOFX Claw402 自动策略 should exist and be active") + t.Fatalf("NOFX Claw402 Auto Strategy should exist and be active") } trendCfg, err := defaultStrategy.ParseConfig() if err != nil { @@ -79,7 +79,7 @@ func TestCreateDefaultStrategiesMigratesLegacyPresetsWithoutOverridingActiveCust legacy := &store.Strategy{ ID: uuid.New().String(), UserID: userID, - Name: "均衡策略", + Name: "Balanced Strategy", Description: "legacy", IsActive: false, } @@ -124,11 +124,11 @@ func TestCreateDefaultStrategiesMigratesLegacyPresetsWithoutOverridingActiveCust activeNames = append(activeNames, strategy.Name) } } - if byName["均衡策略"] != 0 { + if byName["Balanced Strategy"] != 0 { t.Fatalf("legacy preset should be removed, got names=%+v", byName) } - if byName["NOFX Claw402 自动策略"] != 1 { - t.Fatalf("expected exactly one NOFX Claw402 自动策略, got names=%+v", byName) + if byName["NOFX Claw402 Auto Strategy"] != 1 { + t.Fatalf("expected exactly one NOFX Claw402 Auto Strategy, got names=%+v", byName) } if len(activeNames) != 1 || activeNames[0] != "aa" { t.Fatalf("existing active custom strategy should stay the only active one, got %+v", activeNames) diff --git a/api/handler_vergex.go b/api/handler_vergex.go index 665f0690..9f5b0d1d 100644 --- a/api/handler_vergex.go +++ b/api/handler_vergex.go @@ -73,6 +73,28 @@ func (s *Server) handleVergexCostLiquidationHeatmap(c *gin.Context) { c.Data(http.StatusOK, "application/json; charset=utf-8", body) } +// handleVergexFlowMarkets proxies the Vergex net-flow market ranking (paid x402 +// endpoint) using the caller's claw402 wallet. The upstream JSON is passed +// through verbatim: { data: { window, by, inflow: [{ symbol, netFlow, +// buyNotional, sellNotional, trades, latestPrice }, ...] } }. +func (s *Server) handleVergexFlowMarkets(c *gin.Context) { + client, ok := s.newVergexClientForRequest(c) + if !ok { + return + } + chain := withDefault(strings.TrimSpace(c.Query("chain")), "mainnet") + window := withDefault(strings.TrimSpace(c.Query("window")), "1h") + limit := parsePositiveInt(c.Query("limit"), 25) + + body, err := client.GetFlowMarkets(context.Background(), chain, window, limit) + if err != nil { + logger.Warnf("Vergex flow-markets failed: %v", err) + c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) + return + } + c.Data(http.StatusOK, "application/json; charset=utf-8", body) +} + func (s *Server) newVergexClientForRequest(c *gin.Context) (*vergex.Client, bool) { userID := c.GetString("user_id") if userID == "" { diff --git a/api/server.go b/api/server.go index cf608d5d..d3b0d641 100644 --- a/api/server.go +++ b/api/server.go @@ -214,6 +214,7 @@ func (s *Server) setupRoutes() { s.route(protected, "GET", "/vergex/signal-ranking", "Vergex signal ranking via claw402 (?marketType=all&limit=30)", s.handleVergexSignalRanking) s.route(protected, "GET", "/vergex/signal-lab", "Vergex signal lab via claw402 (?marketType=hip3_perp&symbol=AAPL)", s.handleVergexSignalLab) s.route(protected, "GET", "/vergex/cost-liquidation-heatmap", "Vergex cost/liquidation heatmap via claw402 (?marketType=hip3_perp&symbol=AAPL)", s.handleVergexCostLiquidationHeatmap) + s.route(protected, "GET", "/vergex/flow-markets", "Vergex net-flow market ranking via claw402 (?chain=mainnet&window=1h&limit=25)", s.handleVergexFlowMarkets) // AI trader management s.routeWithSchema(protected, "GET", "/my-traders", "List user's traders with status", @@ -435,6 +436,10 @@ Returns the most recent AI decision for each symbol analyzed in the last scan cy `Query: ?trader_id= Returns: {"total_trades":,"winning_trades":,"win_rate":,"total_pnl":,"sharpe_ratio":,"max_drawdown":}`, s.handleStatistics) + s.routeWithSchema(protected, "GET", "/statistics/full", "Full trade-quality metrics (win rate, profit factor, Sharpe, max drawdown)", + `Query: ?trader_id= +Returns: {"total_trades","win_trades","loss_trades","win_rate","profit_factor","sharpe_ratio","total_pnl","total_fee","avg_win","avg_loss","max_drawdown_pct"}`, + s.handleStatisticsFull) } } diff --git a/kernel/engine.go b/kernel/engine.go index c994ca90..c538a566 100644 --- a/kernel/engine.go +++ b/kernel/engine.go @@ -799,14 +799,51 @@ func (e *StrategyEngine) getVergexSignalCoins(limit int, marketType, chain, liqB return candidates, nil } - items := make([]vergex.SignalRankItem, 0, limit) + // Direction-balanced selection: interleave the top bullish and top bearish + // signals so the candidate universe carries BOTH long and short ideas every + // cycle (instead of filling up with whichever bias ranks highest). This is + // what lets the AI actually judge — and trade — both directions. + var bullItems, bearItems, otherItems []vergex.SignalRankItem for _, item := range rankedItems { if category != "" && category != "all" && item.Category != category { continue } - items = append(items, item) - if len(items) >= limit { - break + switch strings.ToLower(strings.TrimSpace(item.Bias)) { + case "bearish", "short", "sell": + bearItems = append(bearItems, item) + case "bullish", "long", "buy": + bullItems = append(bullItems, item) + default: + otherItems = append(otherItems, item) + } + } + items := make([]vergex.SignalRankItem, 0, limit) + bi, ri, oi := 0, 0, 0 + for len(items) < limit { + progressed := false + if bi < len(bullItems) { + items = append(items, bullItems[bi]) + bi++ + progressed = true + if len(items) >= limit { + break + } + } + if ri < len(bearItems) { + items = append(items, bearItems[ri]) + ri++ + progressed = true + if len(items) >= limit { + break + } + } + if !progressed { + if oi < len(otherItems) { + items = append(items, otherItems[oi]) + oi++ + } else { + break + } } } if len(items) == 0 { @@ -840,6 +877,47 @@ func minInt(a, b int) int { return b } +// DirectionalCandidates returns bullish (long) and bearish (short) candidate +// symbols from the most recent Vergex signal ranking, each ordered by upstream +// rank (strongest first). Only populated for vergex_signal coin sources, since +// that is the only source carrying a per-symbol directional bias. +func (e *StrategyEngine) DirectionalCandidates() (bullish []string, bearish []string) { + if e == nil || len(e.vergexRankingCache) == 0 { + return nil, nil + } + type ranked struct { + sym string + rank int + } + rankKey := func(r int) int { + if r > 0 { + return r + } + return 1 << 30 + } + var bl, br []ranked + for sym, item := range e.vergexRankingCache { + if item == nil { + continue + } + switch strings.ToLower(strings.TrimSpace(item.Bias)) { + case "bearish", "short", "sell": + br = append(br, ranked{sym, item.Rank}) + case "bullish", "long", "buy": + bl = append(bl, ranked{sym, item.Rank}) + } + } + sort.SliceStable(bl, func(i, j int) bool { return rankKey(bl[i].rank) < rankKey(bl[j].rank) }) + sort.SliceStable(br, func(i, j int) bool { return rankKey(br[i].rank) < rankKey(br[j].rank) }) + for _, r := range bl { + bullish = append(bullish, r.sym) + } + for _, r := range br { + bearish = append(bearish, r.sym) + } + return bullish, bearish +} + func withDefaultText(value, fallback string) string { if strings.TrimSpace(value) == "" { return fallback diff --git a/kernel/engine_directional_test.go b/kernel/engine_directional_test.go new file mode 100644 index 00000000..9aa4ccab --- /dev/null +++ b/kernel/engine_directional_test.go @@ -0,0 +1,36 @@ +package kernel + +import ( + "testing" + + "nofx/provider/vergex" +) + +func TestDirectionalCandidates(t *testing.T) { + e := &StrategyEngine{ + vergexRankingCache: map[string]*vergex.SignalRankItem{ + "xyz:NVDA": {Symbol: "xyz:NVDA", Bias: "bullish", Rank: 2}, + "xyz:AAPL": {Symbol: "xyz:AAPL", Bias: "bullish", Rank: 1}, + "BTC": {Symbol: "BTC", Bias: "bearish", Rank: 3}, + "ETH": {Symbol: "ETH", Bias: "bearish", Rank: 1}, + "SOL": {Symbol: "SOL", Bias: "neutral", Rank: 1}, + }, + } + + bull, bear := e.DirectionalCandidates() + + if len(bull) != 2 || bull[0] != "xyz:AAPL" || bull[1] != "xyz:NVDA" { + t.Fatalf("bullish should be rank-ordered [xyz:AAPL xyz:NVDA], got %v", bull) + } + if len(bear) != 2 || bear[0] != "ETH" || bear[1] != "BTC" { + t.Fatalf("bearish should be rank-ordered [ETH BTC], got %v", bear) + } +} + +func TestDirectionalCandidatesEmpty(t *testing.T) { + e := &StrategyEngine{} + bull, bear := e.DirectionalCandidates() + if len(bull) != 0 || len(bear) != 0 { + t.Fatalf("empty cache should yield no candidates, got %v %v", bull, bear) + } +} diff --git a/kernel/engine_prompt.go b/kernel/engine_prompt.go index 75a84ad4..6c0eb35c 100644 --- a/kernel/engine_prompt.go +++ b/kernel/engine_prompt.go @@ -42,8 +42,8 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string sb.WriteString(roleDefinition) sb.WriteString("\n\n") } else if zh { - sb.WriteString("# 你是一名专业的 Hyperliquid USDC 多资产交易 AI\n\n") - sb.WriteString("你的任务是基于提供的市场数据做出交易决策。\n\n") + sb.WriteString("# You are a professional Hyperliquid USDC multi-asset trading AI\n\n") + sb.WriteString("Your task is to make trading decisions based on the provided market data.\n\n") } else { sb.WriteString("# You are a professional Hyperliquid USDC multi-asset trading AI\n\n") sb.WriteString("Your task is to make trading decisions based on the provided market data.\n\n") @@ -75,11 +75,11 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string sb.WriteString(tradingFrequency) sb.WriteString("\n\n") } else if zh { - sb.WriteString("# ⏱️ 交易频率提醒\n\n") - sb.WriteString("- 优秀交易员: 每日 2-4 单 ≈ 每小时 0.1-0.2 单\n") - sb.WriteString("- 每小时 > 2 单 = 过度交易\n") - sb.WriteString("- 单笔持仓时长 ≥ 45-90 分钟\n") - sb.WriteString("如果你发现自己每个周期都在交易 → 入场标准过低; 如果不到 45 分钟就平仓 → 太冲动。\n\n") + sb.WriteString("# ⏱️ Trading Frequency Awareness\n\n") + sb.WriteString("- Excellent traders: 2-4 trades/day ≈ 0.1-0.2 trades/hour\n") + sb.WriteString("- >2 trades/hour = overtrading\n") + sb.WriteString("- Single position hold time ≥ 45-90 minutes\n") + sb.WriteString("If you find yourself trading every cycle → standards too low; if closing positions < 45 minutes → too impulsive.\n\n") } else { sb.WriteString("# ⏱️ Trading Frequency Awareness\n\n") sb.WriteString("- Excellent traders: 2-4 trades/day ≈ 0.1-0.2 trades/hour\n") @@ -93,21 +93,21 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string if entryStandards != "" { sb.WriteString(entryStandards) if zh { - sb.WriteString("\n\n你拥有以下指标数据:\n") + sb.WriteString("\n\nYou have the following indicator data:\n") } else { sb.WriteString("\n\nYou have the following indicator data:\n") } e.writeAvailableIndicators(&sb, zh) if zh { - sb.WriteString(fmt.Sprintf("\n**置信度 ≥ %d** 才能开仓。\n\n", riskControl.MinConfidence)) + sb.WriteString(fmt.Sprintf("\n**Confidence ≥ %d** required to open positions.\n\n", riskControl.MinConfidence)) } else { sb.WriteString(fmt.Sprintf("\n**Confidence ≥ %d** required to open positions.\n\n", riskControl.MinConfidence)) } } else if zh { - sb.WriteString("# 🎯 入场标准 (严格)\n\n") - sb.WriteString("只有当多重信号共振时才开仓。你拥有:\n") + sb.WriteString("# 🎯 Entry Standards (Strict)\n\n") + sb.WriteString("Only open positions when multiple signals resonate. You have:\n") e.writeAvailableIndicators(&sb, zh) - sb.WriteString(fmt.Sprintf("\n请自由使用任何有效的分析方法, 但**置信度 ≥ %d** 才能开仓; 避免低质量行为, 如单一指标、信号矛盾、横盘震荡、平仓后立刻再开等。\n\n", riskControl.MinConfidence)) + sb.WriteString(fmt.Sprintf("\nFeel free to use any effective analysis method, but **confidence ≥ %d** is required to open positions; avoid low-quality behaviors such as single-indicator entries, contradictory signals, sideways chop, or re-entering immediately after a close.\n\n", riskControl.MinConfidence)) } else { sb.WriteString("# 🎯 Entry Standards (Strict)\n\n") sb.WriteString("Only open positions when multiple signals resonate. You have:\n") @@ -121,10 +121,10 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string sb.WriteString(decisionProcess) sb.WriteString("\n\n") } else if zh { - sb.WriteString("# 📋 决策流程\n\n") - sb.WriteString("1. 检查持仓 → 是否需要止盈止损\n") - sb.WriteString("2. 扫描候选标的 + 多周期 → 是否有强信号\n") - sb.WriteString("3. 先写思维链, 再输出结构化 JSON\n\n") + sb.WriteString("# 📋 Decision Process\n\n") + sb.WriteString("1. Check positions → take profit / stop loss?\n") + sb.WriteString("2. Scan candidates + multi-timeframe → are there strong signals?\n") + sb.WriteString("3. Write chain of thought first, then output structured JSON\n\n") } else { sb.WriteString("# 📋 Decision Process\n\n") sb.WriteString("1. Check positions → take profit / stop loss?\n") @@ -153,14 +153,14 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string if customPrompt != "" { if zh { - sb.WriteString("# 📌 个性化交易策略\n\n") + sb.WriteString("# 📌 Personalized Trading Strategy\n\n") } else { sb.WriteString("# 📌 Personalized Trading Strategy\n\n") } sb.WriteString(customPrompt) sb.WriteString("\n\n") if zh { - sb.WriteString("说明: 上述个性化策略是基础规则的补充, 不能违反基础风控原则。\n") + sb.WriteString("Note: the above personalized strategy supplements the basic rules and may not violate the core risk controls.\n") } else { sb.WriteString("Note: the above personalized strategy supplements the basic rules and may not violate the core risk controls.\n") } @@ -191,21 +191,21 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant sb.WriteString("\n\n---\n\n") if zh { - sb.WriteString("# 你是 NOFX Claw402 自动交易员\n\n") - sb.WriteString("你的任务是交易 Claw402.ai/Vergex 本轮榜单返回的 Hyperliquid 可交易标的。只允许交易本轮候选标的和已有持仓,不要自行发明代码或切换到榜单外标的。\n\n") - sb.WriteString("# 决策数据优先级\n\n") - sb.WriteString("1. Claw402.ai Signal Ranking: 决定本轮候选池、排名、方向和类别。\n") - sb.WriteString("2. Claw402.ai Signal Lab: 用于确认趋势、动量、事件或模型信号;这是开仓前的核心确认数据。\n") - sb.WriteString("3. Claw402.ai Cost/Liquidation Heatmap: 用于识别清算密集区、成本区、止损位置和止盈目标。\n") - sb.WriteString("4. 原始 OHLCV K 线: 用于验证入场时机、趋势结构、波动和风险回报。\n\n") - sb.WriteString("# 交易原则\n\n") - sb.WriteString("- 先管理已有持仓,再考虑新开仓。\n") - sb.WriteString("- 开仓需要 Signal Lab、热力图和 K 线方向大体一致;任一关键数据缺失或互相冲突时,默认等待。\n") - sb.WriteString("- 不要把 Claw402 排名当作唯一买入理由;排名只是候选池,开仓必须经过详情数据和 K 线确认。\n") - sb.WriteString("- 本轮 Candidate Coins 中的标的都是允许交易的候选;如果某个标的详情缺失,只能降低置信度或等待,不能说它不属于可交易范围。\n") - sb.WriteString("- 如果 Signal Lab 或热力图没有出现在该标的的 Vergex Claw402 Signals 里,必须在 reasoning 中说明缺失;如果已经出现,则不能声称该标的缺少该数据。\n") - sb.WriteString("- 防止频繁开平仓:非止损或强止盈情况下,开仓后至少持有 45 分钟;小亏小赚的噪音区优先持有到 90 分钟;平仓后同一标的 90 分钟内不重新进场;每小时最多 1 次新开仓。\n") - sb.WriteString("- 止损必须放在无效点之外;止盈优先放在热力图阻力/清算区域或满足风险回报的位置。\n\n") + sb.WriteString("# You are the NOFX Claw402 auto-trader\n\n") + sb.WriteString("Trade only Hyperliquid instruments returned by this cycle's Claw402.ai/Vergex board. You may trade only the current candidate symbols and existing positions; never invent tickers or rotate outside the provided universe.\n\n") + sb.WriteString("# Decision Data Priority\n\n") + sb.WriteString("1. Claw402.ai Signal Ranking: candidate pool, rank, direction and category.\n") + sb.WriteString("2. Claw402.ai Signal Lab: trend, momentum, event/model confirmation; this is the core pre-entry confirmation source.\n") + sb.WriteString("3. Claw402.ai Cost/Liquidation Heatmap: crowded liquidation/cost zones, stop placement and target zones.\n") + sb.WriteString("4. Raw OHLCV candles: entry timing, trend structure, volatility and risk/reward validation.\n\n") + sb.WriteString("# Trading Rules\n\n") + sb.WriteString("- Manage existing positions before opening new ones.\n") + sb.WriteString("- Open only when Signal Lab, heatmap and raw candles broadly agree; wait when key data is missing or contradictory.\n") + sb.WriteString("- Ranking alone is not an entry reason; it only defines the candidate pool.\n") + sb.WriteString("- Every symbol in Candidate Coins is part of the allowed trading universe; missing detail can lower confidence or trigger waiting, but does not make the symbol non-tradable.\n") + sb.WriteString("- If Signal Lab or heatmap is absent from that symbol's Vergex Claw402 Signals, state it in reasoning; if it is present, never claim the symbol lacks that data.\n") + sb.WriteString("- Avoid churn: unless stopping out or taking a strong profit, hold new positions for at least 45 minutes; avoid flat/noise closes until roughly 90 minutes; after closing a symbol, wait 90 minutes before re-entry; open at most 1 new position per hour.\n") + sb.WriteString("- Stops must sit beyond invalidation; targets should prefer heatmap resistance/liquidation zones or valid risk/reward levels.\n\n") } else { sb.WriteString("# You are the NOFX Claw402 auto-trader\n\n") sb.WriteString("Trade only Hyperliquid instruments returned by this cycle's Claw402.ai/Vergex board. You may trade only the current candidate symbols and existing positions; never invent tickers or rotate outside the provided universe.\n\n") @@ -256,15 +256,15 @@ func englishOnlyPromptSection(section string) string { func writeVergexSchemaPrompt(sb *strings.Builder, zh bool) { if zh { - sb.WriteString("# Claw402.ai TradeFi 数据说明\n\n") - sb.WriteString("- Equity: 账户总权益,包含浮动盈亏,单位 USDT。\n") - sb.WriteString("- Balance: 可用余额,用于判断还能否开新仓,单位 USDT。\n") - sb.WriteString("- Margin: 当前保证金使用率,越高风险越大。\n") - sb.WriteString("- Position: 当前持仓,包含方向、进场价、杠杆、未实现盈亏、强平价。\n") - sb.WriteString("- Claw402 Ranking: 本轮可交易候选池、排名、方向和类别。\n") - sb.WriteString("- Signal Lab: Claw402 对单个标的的深度信号,用于确认趋势和质量。\n") - sb.WriteString("- Cost/Liquidation Heatmap: 成本区与清算密集区,用于止损、止盈和拥挤风险判断。\n") - sb.WriteString("- Raw OHLCV Kline: 原始 K 线,用于确认趋势结构、入场位置和风险回报。\n") + sb.WriteString("# Claw402.ai TradeFi Data Guide\n\n") + sb.WriteString("- Equity: total account value including unrealized PnL, in USDT.\n") + sb.WriteString("- Balance: available balance for new positions, in USDT.\n") + sb.WriteString("- Margin: current margin usage; higher means more risk.\n") + sb.WriteString("- Position: current holdings with side, entry, leverage, unrealized PnL and liquidation price.\n") + sb.WriteString("- Claw402 Ranking: tradable candidate pool, rank, direction and category for this cycle.\n") + sb.WriteString("- Signal Lab: per-symbol Claw402 deep signal used to confirm trend and quality.\n") + sb.WriteString("- Cost/Liquidation Heatmap: cost and liquidation clusters used for stops, targets and crowding risk.\n") + sb.WriteString("- Raw OHLCV Kline: raw candles used for trend structure, entry timing and risk/reward.\n") } else { sb.WriteString("# Claw402.ai TradeFi Data Guide\n\n") sb.WriteString("- Equity: total account value including unrealized PnL, in USDT.\n") @@ -281,22 +281,22 @@ func writeVergexSchemaPrompt(sb *strings.Builder, zh bool) { func writeVergexHardConstraints(sb *strings.Builder, accountEquity float64, riskControl store.RiskControlConfig, tradeFiPositionValueRatio float64, zh bool) { maxPositionValue := accountEquity * tradeFiPositionValueRatio if zh { - sb.WriteString("# 风控硬约束\n\n") - sb.WriteString("## 后端强制\n") - sb.WriteString(fmt.Sprintf("- 最大持仓数: 同时 %d 个 Claw402 候选标的\n", riskControl.MaxPositions)) - sb.WriteString(fmt.Sprintf("- 单仓最大名义价值: %.0f USDT (= 权益 %.0f × %.1fx)\n", maxPositionValue, accountEquity, tradeFiPositionValueRatio)) - sb.WriteString(fmt.Sprintf("- 最大保证金占用: ≤%.0f%%\n", riskControl.MaxMarginUsage*100)) - sb.WriteString(fmt.Sprintf("- 最小下单金额: ≥%.0f USDT\n\n", riskControl.MinPositionSize)) - sb.WriteString("## AI 建议\n") - sb.WriteString(fmt.Sprintf("- 交易杠杆: Claw402 候选标的最高 %dx\n", riskControl.AltcoinMaxLeverage)) - sb.WriteString(fmt.Sprintf("- 风险回报比: ≥1:%.1f\n", riskControl.MinRiskRewardRatio)) - sb.WriteString(fmt.Sprintf("- 最小置信度: ≥%d 才能开仓\n\n", riskControl.MinConfidence)) - sb.WriteString("# 仓位大小\n\n") - sb.WriteString("根据置信度和单仓最大名义价值填写 `position_size_usd`:\n") - sb.WriteString("- 高置信 (≥85): 使用上限的 80-100%\n") - sb.WriteString("- 中置信 (70-84): 使用上限的 50-80%\n") - sb.WriteString("- 低置信 (60-69): 使用上限的 30-50%\n") - sb.WriteString("- 不要直接把 available_balance 当作 position_size_usd。\n\n") + sb.WriteString("# Hard Risk Constraints\n\n") + sb.WriteString("## Backend enforced\n") + sb.WriteString(fmt.Sprintf("- Max positions: %d Claw402 candidate instruments at the same time\n", riskControl.MaxPositions)) + sb.WriteString(fmt.Sprintf("- Max notional per position: %.0f USDT (= equity %.0f × %.1fx)\n", maxPositionValue, accountEquity, tradeFiPositionValueRatio)) + sb.WriteString(fmt.Sprintf("- Max margin usage: ≤%.0f%%\n", riskControl.MaxMarginUsage*100)) + sb.WriteString(fmt.Sprintf("- Min order size: ≥%.0f USDT\n\n", riskControl.MinPositionSize)) + sb.WriteString("## AI guided\n") + sb.WriteString(fmt.Sprintf("- Leverage: every open position must use exactly %dx\n", riskControl.AltcoinMaxLeverage)) + sb.WriteString(fmt.Sprintf("- Risk/reward: ≥1:%.1f\n", riskControl.MinRiskRewardRatio)) + sb.WriteString(fmt.Sprintf("- Min confidence to open: ≥%d\n\n", riskControl.MinConfidence)) + sb.WriteString("# Position Sizing\n\n") + sb.WriteString("For every `open_long` or `open_short`, use the full max notional per position.\n") + sb.WriteString("- Do not scale position_size_usd down by confidence.\n") + sb.WriteString("- Do not open small probe positions.\n") + sb.WriteString("- If the setup is not strong enough for full size, output `wait`.\n") + sb.WriteString("- Do not use available_balance directly as position_size_usd.\n\n") } else { sb.WriteString("# Hard Risk Constraints\n\n") sb.WriteString("## Backend enforced\n") @@ -332,15 +332,21 @@ func writeVergexOutputFormat(sb *strings.Builder, accountEquity float64, riskCon sb.WriteString("# Output Format (Strictly Follow)\n\n") if zh { - sb.WriteString("必须使用 XML 标签 分隔简明分析和决策 JSON。\n\n") - sb.WriteString("方向必须由数据决定:上涨结构确认时可以 `open_long`,下跌结构确认时可以 `open_short`;不要默认只做多或只做空。\n\n") + sb.WriteString("Use XML tags and to separate concise analysis from the decision JSON.\n\n") + sb.WriteString("Direction must be data-driven: use `open_long` for confirmed upside structures and `open_short` for confirmed downside structures; never default to long-only or short-only behavior.\n\n") + if !singleSymbol { + sb.WriteString("This cycle you MUST include at least one `open_long` (pick the strongest net-inflow / bullish name) AND at least one `open_short` (pick the strongest net-outflow / bearish name); omit a side only if no suitable name exists for it.\n\n") + } } else { sb.WriteString("Use XML tags and to separate concise analysis from the decision JSON.\n\n") sb.WriteString("Direction must be data-driven: use `open_long` for confirmed upside structures and `open_short` for confirmed downside structures; never default to long-only or short-only behavior.\n\n") + if !singleSymbol { + sb.WriteString("This cycle you MUST include at least one `open_long` (pick the strongest net-inflow / bullish name) AND at least one `open_short` (pick the strongest net-outflow / bearish name); omit a side only if no suitable name exists for it.\n\n") + } } sb.WriteString("\n") if zh { - sb.WriteString("简明说明: Claw402 排名、Signal Lab、热力图、K 线是否一致;如果缺数据或冲突,说明为什么等待。\n") + sb.WriteString("Briefly state whether Claw402 ranking, Signal Lab, heatmap and candles agree; if data is missing or conflicting, explain why you wait.\n") } else { sb.WriteString("Briefly state whether Claw402 ranking, Signal Lab, heatmap and candles agree; if data is missing or conflicting, explain why you wait.\n") } @@ -357,15 +363,15 @@ func writeVergexOutputFormat(sb *strings.Builder, accountEquity float64, riskCon sb.WriteString("\n\n") if zh { - sb.WriteString("## 字段要求\n\n") + sb.WriteString("## Field Requirements\n\n") sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n") - sb.WriteString(fmt.Sprintf("- `confidence`: 0-100,开仓建议 ≥ %d\n", riskControl.MinConfidence)) - sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n") - sb.WriteString("- 所有数值必须是算好的数字,不能写公式。\n") + sb.WriteString(fmt.Sprintf("- `confidence`: 0-100; recommended ≥ %d to open\n", riskControl.MinConfidence)) + sb.WriteString("- Required when opening: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n") + sb.WriteString("- All numeric values must be calculated numbers, not formulas.\n") if singleSymbol { - sb.WriteString(fmt.Sprintf("- 本策略只交易 `%s`,JSON 的 symbol 必须完全等于它。\n", exampleSymbol)) + sb.WriteString(fmt.Sprintf("- This strategy trades only `%s`; JSON symbol must match it exactly.\n", exampleSymbol)) } else { - sb.WriteString("- JSON 的 symbol 必须完全来自本轮候选标的或已有持仓;`xyz:` 标的保留前缀,core crypto 标的不要添加 `xyz:` 或 `USDT` 后缀。\n") + sb.WriteString("- JSON symbols must exactly match current candidates or existing positions; keep `xyz:` on XYZ instruments, and do not add `xyz:` or `USDT` to core crypto symbols.\n") } sb.WriteString("\n") } else { @@ -446,19 +452,19 @@ func writeModeVariant(sb *strings.Builder, variant string, zh bool) { switch strings.ToLower(strings.TrimSpace(variant)) { case "aggressive": if zh { - sb.WriteString("## 模式: 激进\n- 优先捕捉趋势突破, 置信度 ≥ 70 时可分批建仓\n- 允许更高仓位, 但必须严格止损并说明风险回报比\n\n") + sb.WriteString("## Mode: Aggressive\n- Prioritize capturing trend breakouts; may scale in when confidence ≥ 70\n- Allow larger positions, but must strictly set stop-loss and explain the risk-reward ratio\n\n") } else { sb.WriteString("## Mode: Aggressive\n- Prioritize capturing trend breakouts; may scale in when confidence ≥ 70\n- Allow larger positions, but must strictly set stop-loss and explain the risk-reward ratio\n\n") } case "conservative": if zh { - sb.WriteString("## 模式: 保守\n- 只有当多重信号共振时才开仓\n- 优先保本, 连亏后必须暂停多个周期\n\n") + sb.WriteString("## Mode: Conservative\n- Open positions only when multiple signals resonate\n- Prioritize capital preservation; pause for multiple periods after consecutive losses\n\n") } else { sb.WriteString("## Mode: Conservative\n- Open positions only when multiple signals resonate\n- Prioritize capital preservation; pause for multiple periods after consecutive losses\n\n") } case "scalping": if zh { - sb.WriteString("## 模式: 短线\n- 关注短期动量, 利润目标较小但要求迅速行动\n- 价格两根 K 线内未按预期走 → 立即减仓或止损\n\n") + sb.WriteString("## Mode: Scalping\n- Focus on short-term momentum, smaller profit targets but require quick action\n- If price doesn't move as expected within two bars, immediately reduce position or stop-loss\n\n") } else { sb.WriteString("## Mode: Scalping\n- Focus on short-term momentum, smaller profit targets but require quick action\n- If price doesn't move as expected within two bars, immediately reduce position or stop-loss\n\n") } @@ -467,9 +473,9 @@ func writeModeVariant(sb *strings.Builder, variant string, zh bool) { func writeHardConstraints(sb *strings.Builder, accountEquity float64, riskControl store.RiskControlConfig, btcEthPosValueRatio, altcoinPosValueRatio float64, singleSymbol bool, primarySymbol string, zh bool) { if zh { - sb.WriteString("# 风控硬约束\n\n") - sb.WriteString("## 代码强制 (后端校验, 无法绕过):\n") - sb.WriteString(fmt.Sprintf("- 最大持仓数: 同时 %d 个标的\n", riskControl.MaxPositions)) + sb.WriteString("# Hard Constraints (Risk Control)\n\n") + sb.WriteString("## CODE ENFORCED (backend validation, cannot be bypassed):\n") + sb.WriteString(fmt.Sprintf("- Max Positions: %d instruments simultaneously\n", riskControl.MaxPositions)) } else { sb.WriteString("# Hard Constraints (Risk Control)\n\n") sb.WriteString("## CODE ENFORCED (backend validation, cannot be bypassed):\n") @@ -486,14 +492,14 @@ func writeHardConstraints(sb *strings.Builder, accountEquity float64, riskContro maxVal := accountEquity * ratio symLabel := primarySymbol if zh { - sb.WriteString(fmt.Sprintf("- 单仓最大价值 (%s): %.0f USDT (= 权益 %.0f × %.1fx)\n", symLabel, maxVal, accountEquity, ratio)) + sb.WriteString(fmt.Sprintf("- Position Value Limit (%s): max %.0f USDT (= equity %.0f × %.1fx)\n", symLabel, maxVal, accountEquity, ratio)) } else { sb.WriteString(fmt.Sprintf("- Position Value Limit (%s): max %.0f USDT (= equity %.0f × %.1fx)\n", symLabel, maxVal, accountEquity, ratio)) } } else { if zh { - sb.WriteString(fmt.Sprintf("- 单仓最大价值 (山寨币/股票): %.0f USDT (= 权益 %.0f × %.1fx)\n", accountEquity*altcoinPosValueRatio, accountEquity, altcoinPosValueRatio)) - sb.WriteString(fmt.Sprintf("- 单仓最大价值 (BTC/ETH): %.0f USDT (= 权益 %.0f × %.1fx)\n", accountEquity*btcEthPosValueRatio, accountEquity, btcEthPosValueRatio)) + sb.WriteString(fmt.Sprintf("- Position Value Limit (Altcoin/Stock): max %.0f USDT (= equity %.0f × %.1fx)\n", accountEquity*altcoinPosValueRatio, accountEquity, altcoinPosValueRatio)) + sb.WriteString(fmt.Sprintf("- Position Value Limit (BTC/ETH): max %.0f USDT (= equity %.0f × %.1fx)\n", accountEquity*btcEthPosValueRatio, accountEquity, btcEthPosValueRatio)) } else { sb.WriteString(fmt.Sprintf("- Position Value Limit (Altcoin/Stock): max %.0f USDT (= equity %.0f × %.1fx)\n", accountEquity*altcoinPosValueRatio, accountEquity, altcoinPosValueRatio)) sb.WriteString(fmt.Sprintf("- Position Value Limit (BTC/ETH): max %.0f USDT (= equity %.0f × %.1fx)\n", accountEquity*btcEthPosValueRatio, accountEquity, btcEthPosValueRatio)) @@ -501,9 +507,9 @@ func writeHardConstraints(sb *strings.Builder, accountEquity float64, riskContro } if zh { - sb.WriteString(fmt.Sprintf("- 最大保证金占用: ≤%.0f%%\n", riskControl.MaxMarginUsage*100)) - sb.WriteString(fmt.Sprintf("- 最小下单金额: ≥%.0f USDT\n\n", riskControl.MinPositionSize)) - sb.WriteString("## AI 建议 (推荐遵循):\n") + sb.WriteString(fmt.Sprintf("- Max Margin Usage: ≤%.0f%%\n", riskControl.MaxMarginUsage*100)) + sb.WriteString(fmt.Sprintf("- Min Position Size: ≥%.0f USDT\n\n", riskControl.MinPositionSize)) + sb.WriteString("## AI GUIDED (recommended):\n") } else { sb.WriteString(fmt.Sprintf("- Max Margin Usage: ≤%.0f%%\n", riskControl.MaxMarginUsage*100)) sb.WriteString(fmt.Sprintf("- Min Position Size: ≥%.0f USDT\n\n", riskControl.MinPositionSize)) @@ -516,20 +522,20 @@ func writeHardConstraints(sb *strings.Builder, accountEquity float64, riskContro lev = riskControl.BTCETHMaxLeverage } if zh { - sb.WriteString(fmt.Sprintf("- 交易杠杆 (%s): 最高 %dx\n", primarySymbol, lev)) + sb.WriteString(fmt.Sprintf("- Trading Leverage (%s): max %dx\n", primarySymbol, lev)) } else { sb.WriteString(fmt.Sprintf("- Trading Leverage (%s): max %dx\n", primarySymbol, lev)) } } else { if zh { - sb.WriteString(fmt.Sprintf("- 交易杠杆: 山寨币/股票 最高 %dx | BTC/ETH 最高 %dx\n", riskControl.AltcoinMaxLeverage, riskControl.BTCETHMaxLeverage)) + sb.WriteString(fmt.Sprintf("- Trading Leverage: Altcoin/Stock max %dx | BTC/ETH max %dx\n", riskControl.AltcoinMaxLeverage, riskControl.BTCETHMaxLeverage)) } else { sb.WriteString(fmt.Sprintf("- Trading Leverage: Altcoin/Stock max %dx | BTC/ETH max %dx\n", riskControl.AltcoinMaxLeverage, riskControl.BTCETHMaxLeverage)) } } if zh { - sb.WriteString(fmt.Sprintf("- 风险回报比: ≥1:%.1f (take_profit / stop_loss)\n", riskControl.MinRiskRewardRatio)) - sb.WriteString(fmt.Sprintf("- 最小置信度: ≥%d 才开仓\n\n", riskControl.MinConfidence)) + sb.WriteString(fmt.Sprintf("- Risk-Reward Ratio: ≥1:%.1f (take_profit / stop_loss)\n", riskControl.MinRiskRewardRatio)) + sb.WriteString(fmt.Sprintf("- Min Confidence: ≥%d to open position\n\n", riskControl.MinConfidence)) } else { sb.WriteString(fmt.Sprintf("- Risk-Reward Ratio: ≥1:%.1f (take_profit / stop_loss)\n", riskControl.MinRiskRewardRatio)) sb.WriteString(fmt.Sprintf("- Min Confidence: ≥%d to open position\n\n", riskControl.MinConfidence)) @@ -544,13 +550,13 @@ func writeHardConstraints(sb *strings.Builder, accountEquity float64, riskContro } } if zh { - sb.WriteString("## 仓位大小指引\n") - sb.WriteString("根据置信度和上面的单仓最大价值算出 `position_size_usd`:\n") - sb.WriteString("- 高置信 (≥85): 用最大价值的 80-100%%\n") - sb.WriteString("- 中置信 (70-84): 用最大价值的 50-80%%\n") - sb.WriteString("- 低置信 (60-69): 用最大价值的 30-50%%\n") - sb.WriteString(fmt.Sprintf("- 示例: 权益 %.0f × %.1fx = 最大 %.0f USDT\n", accountEquity, exampleRatio, accountEquity*exampleRatio)) - sb.WriteString("- **不要**直接拿 available_balance 当 position_size_usd, 用上面的单仓最大价值!\n\n") + sb.WriteString("## Position Sizing Guidance\n") + sb.WriteString("Calculate `position_size_usd` from your confidence and the Position Value Limits above:\n") + sb.WriteString("- High confidence (≥85): use 80-100%% of the position value limit\n") + sb.WriteString("- Medium confidence (70-84): use 50-80%% of the position value limit\n") + sb.WriteString("- Low confidence (60-69): use 30-50%% of the position value limit\n") + sb.WriteString(fmt.Sprintf("- Example: equity %.0f × %.1fx = max %.0f USDT\n", accountEquity, exampleRatio, accountEquity*exampleRatio)) + sb.WriteString("- **DO NOT** just use available_balance as position_size_usd. Use the Position Value Limit!\n\n") } else { sb.WriteString("## Position Sizing Guidance\n") sb.WriteString("Calculate `position_size_usd` from your confidence and the Position Value Limits above:\n") @@ -566,21 +572,21 @@ func writeOutputFormat(sb *strings.Builder, accountEquity, btcEthPosValueRatio f // Output format schema MUST stay English/structural; parser depends on it. sb.WriteString("# Output Format (Strictly Follow)\n\n") if zh { - sb.WriteString("**必须使用 XML 标签 分隔思维链和决策 JSON, 避免解析错误**\n\n") + sb.WriteString("**Must use XML tags and to separate chain of thought and decision JSON, avoiding parsing errors**\n\n") } else { sb.WriteString("**Must use XML tags and to separate chain of thought and decision JSON, avoiding parsing errors**\n\n") } sb.WriteString("## Format Requirements\n\n") sb.WriteString("\n") if zh { - sb.WriteString("你的思维链分析...\n- 简明分析你的思考过程\n") + sb.WriteString("Your chain of thought analysis...\n- Briefly analyze your thinking process\n") } else { sb.WriteString("Your chain of thought analysis...\n- Briefly analyze your thinking process\n") } sb.WriteString("\n\n") sb.WriteString("\n") if zh { - sb.WriteString("步骤 2: JSON 决策数组\n\n") + sb.WriteString("Step 2: JSON decision array\n\n") } else { sb.WriteString("Step 2: JSON decision array\n\n") } @@ -608,13 +614,13 @@ func writeOutputFormat(sb *strings.Builder, accountEquity, btcEthPosValueRatio f sb.WriteString("\n\n") if zh { - sb.WriteString("## 字段说明\n\n") + sb.WriteString("## Field Description\n\n") sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n") - sb.WriteString(fmt.Sprintf("- `confidence`: 0-100 (开仓建议 ≥ %d)\n", riskControl.MinConfidence)) - sb.WriteString("- 开仓时必填: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n") - sb.WriteString("- **重要**: 所有数值必须是算好的数字, 不能是公式/表达式 (例如写 `27.76`, 不要写 `3000 * 0.01`)\n") + sb.WriteString(fmt.Sprintf("- `confidence`: 0-100 (opening recommended ≥ %d)\n", riskControl.MinConfidence)) + sb.WriteString("- Required when opening: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n") + sb.WriteString("- **IMPORTANT**: all numeric values must be calculated numbers, NOT formulas/expressions (e.g. use `27.76`, not `3000 * 0.01`)\n") if singleSymbol { - sb.WriteString(fmt.Sprintf("- **本策略只交易 %s**, JSON 中的 `symbol` 必须**完全等于** `%s`, 不要写成 `%s` 去掉后缀或加 USDT 的变体。\n", primarySymbol, primarySymbol, primarySymbol)) + sb.WriteString(fmt.Sprintf("- **This strategy trades only %s.** The JSON `symbol` MUST match `%s` exactly — do not write `%s` variants that drop the suffix or add USDT.\n", primarySymbol, primarySymbol, primarySymbol)) } sb.WriteString("\n") } else { @@ -642,9 +648,9 @@ func (e *StrategyEngine) writeAvailableIndicators(sb *strings.Builder, zh bool) } if zh { - sb.WriteString(fmt.Sprintf("- %s 价格序列", kline.PrimaryTimeframe)) + sb.WriteString(fmt.Sprintf("- %s price series", kline.PrimaryTimeframe)) if kline.EnableMultiTimeframe { - sb.WriteString(fmt.Sprintf(" + %s K 线序列\n", kline.LongerTimeframe)) + sb.WriteString(fmt.Sprintf(" + %s K-line series\n", kline.LongerTimeframe)) } else { sb.WriteString("\n") } @@ -658,50 +664,50 @@ func (e *StrategyEngine) writeAvailableIndicators(sb *strings.Builder, zh bool) } if indicators.EnableEMA { - sb.WriteString("- " + label("EMA indicators", "EMA 指标")) + sb.WriteString("- " + label("EMA indicators", "EMA indicators")) if len(indicators.EMAPeriods) > 0 { - sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.EMAPeriods)) + sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "periods"), indicators.EMAPeriods)) } sb.WriteString("\n") } if indicators.EnableMACD { - sb.WriteString("- " + label("MACD indicators", "MACD 指标") + "\n") + sb.WriteString("- " + label("MACD indicators", "MACD indicators") + "\n") } if indicators.EnableRSI { - sb.WriteString("- " + label("RSI indicators", "RSI 指标")) + sb.WriteString("- " + label("RSI indicators", "RSI indicators")) if len(indicators.RSIPeriods) > 0 { - sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.RSIPeriods)) + sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "periods"), indicators.RSIPeriods)) } sb.WriteString("\n") } if indicators.EnableATR { - sb.WriteString("- " + label("ATR indicators", "ATR 指标")) + sb.WriteString("- " + label("ATR indicators", "ATR indicators")) if len(indicators.ATRPeriods) > 0 { - sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.ATRPeriods)) + sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "periods"), indicators.ATRPeriods)) } sb.WriteString("\n") } if indicators.EnableBOLL { - sb.WriteString("- " + label("Bollinger Bands (BOLL) - Upper/Middle/Lower bands", "布林带 (BOLL) - 上/中/下轨")) + sb.WriteString("- " + label("Bollinger Bands (BOLL) - Upper/Middle/Lower bands", "Bollinger Bands (BOLL) - Upper/Middle/Lower bands")) if len(indicators.BOLLPeriods) > 0 { - sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "周期"), indicators.BOLLPeriods)) + sb.WriteString(fmt.Sprintf(" (%s: %v)", label("periods", "periods"), indicators.BOLLPeriods)) } sb.WriteString("\n") } if indicators.EnableVolume { - sb.WriteString("- " + label("Volume data", "成交量数据") + "\n") + sb.WriteString("- " + label("Volume data", "Volume data") + "\n") } if indicators.EnableOI { - sb.WriteString("- " + label("Open Interest (OI) data", "持仓量 (OI) 数据") + "\n") + sb.WriteString("- " + label("Open Interest (OI) data", "Open Interest (OI) data") + "\n") } if indicators.EnableFundingRate { - sb.WriteString("- " + label("Funding rate", "资金费率") + "\n") + sb.WriteString("- " + label("Funding rate", "Funding rate") + "\n") } if len(e.config.CoinSource.StaticCoins) > 0 || e.config.CoinSource.UseAI500 || e.config.CoinSource.UseOITop { - sb.WriteString("- " + label("AI500 / OI_Top filter tags (if available)", "AI500 / OI_Top 过滤标记 (如有)") + "\n") + sb.WriteString("- " + label("AI500 / OI_Top filter tags (if available)", "AI500 / OI_Top filter tags (if available)") + "\n") } if indicators.EnableQuantData { - sb.WriteString("- " + label("Quantitative data (institutional/retail fund flow, position changes, multi-period price changes)", "量化数据 (机构/散户资金流, 持仓变化, 多周期价格变动)") + "\n") + sb.WriteString("- " + label("Quantitative data (institutional/retail fund flow, position changes, multi-period price changes)", "Quantitative data (institutional/retail fund flow, position changes, multi-period price changes)") + "\n") } } @@ -762,13 +768,13 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string { } if lang == LangChinese { - sb.WriteString("## 历史交易统计\n") - sb.WriteString(fmt.Sprintf("总交易: %d 笔 | 盈利因子: %.2f | 夏普比率: %.2f | 盈亏比: %.2f\n", + sb.WriteString("## Historical Trading Statistics\n") + sb.WriteString(fmt.Sprintf("Total Trades: %d | Profit Factor: %.2f | Sharpe: %.2f | Win/Loss Ratio: %.2f\n", ctx.TradingStats.TotalTrades, ctx.TradingStats.ProfitFactor, ctx.TradingStats.SharpeRatio, winLossRatio)) - sb.WriteString(fmt.Sprintf("总盈亏: %+.2f USDT | 平均盈利: +%.2f | 平均亏损: -%.2f | 最大回撤: %.1f%%\n", + sb.WriteString(fmt.Sprintf("Total PnL: %+.2f USDT | Avg Win: +%.2f | Avg Loss: -%.2f | Max Drawdown: %.1f%%\n", ctx.TradingStats.TotalPnL, ctx.TradingStats.AvgWin, ctx.TradingStats.AvgLoss, @@ -776,13 +782,13 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string { // Performance hints based on profit factor, sharpe, and drawdown if ctx.TradingStats.ProfitFactor >= 1.5 && ctx.TradingStats.SharpeRatio >= 1 { - sb.WriteString("表现: 良好 - 保持当前策略\n") + sb.WriteString("Performance: GOOD - maintain current strategy\n") } else if ctx.TradingStats.ProfitFactor < 1 { - sb.WriteString("表现: 需改进 - 提高盈亏比,优化止盈止损\n") + sb.WriteString("Performance: NEEDS IMPROVEMENT - improve win/loss ratio, optimize TP/SL\n") } else if ctx.TradingStats.MaxDrawdownPct > 30 { - sb.WriteString("表现: 风险偏高 - 减少仓位,控制回撤\n") + sb.WriteString("Performance: HIGH RISK - reduce position size, control drawdown\n") } else { - sb.WriteString("表现: 正常 - 有优化空间\n") + sb.WriteString("Performance: NORMAL - room for optimization\n") } } else { sb.WriteString("## Historical Trading Statistics\n") diff --git a/kernel/engine_prompt_test.go b/kernel/engine_prompt_test.go index 25a6a338..d6e3a3d0 100644 --- a/kernel/engine_prompt_test.go +++ b/kernel/engine_prompt_test.go @@ -11,8 +11,8 @@ func TestBuildSystemPromptUsesVergexClaw402Prompt(t *testing.T) { cfg := store.GetDefaultStrategyConfig("zh") cfg.CoinSource.SourceType = "vergex_signal" cfg.CoinSource.VergexLimit = 5 - cfg.PromptSections.RoleDefinition = "# 你是一个专业的 Hyperliquid USDC 多资产交易AI" - cfg.CustomPrompt = "只做多,不做空。" + cfg.PromptSections.RoleDefinition = "# You are a professional Hyperliquid USDC multi-asset trading AI" + cfg.CustomPrompt = "Long only, no shorts." engine := NewStrategyEngine(&cfg) prompt := engine.BuildSystemPrompt(30, "balanced") @@ -39,9 +39,9 @@ func TestBuildSystemPromptUsesVergexClaw402Prompt(t *testing.T) { t.Fatalf("system prompt must be English-only, got CJK text:\n%s", prompt) } legacyPhrases := []string{ - "Hyperliquid USDC 多资产交易AI", - "只做多", - "山寨币", + "Hyperliquid USDC multi-asset trading AI", + "Long only", + "Altcoin", "BTC/ETH", "LONG-ONLY", "Do not short", @@ -61,11 +61,11 @@ func TestBuildSystemPromptFallsBackToEnglishWhenConfiguredLanguageIsChinese(t *t cfg.CoinSource.VergexLimit = 0 cfg.CoinSource.VergexMarketType = "" cfg.CoinSource.VergexChain = "" - cfg.PromptSections.RoleDefinition = "# 你是中文系统提示词" - cfg.PromptSections.TradingFrequency = "# 高频交易\n每分钟交易。" - cfg.PromptSections.EntryStandards = "# 入场\n随便开仓。" - cfg.PromptSections.DecisionProcess = "# 决策\n直接输出。" - cfg.CustomPrompt = "中文偏好不应进入系统提示词。" + cfg.PromptSections.RoleDefinition = "# You are a Chinese system prompt" + cfg.PromptSections.TradingFrequency = "# High-frequency trading\nTrade every minute." + cfg.PromptSections.EntryStandards = "# Entry\nOpen positions freely." + cfg.PromptSections.DecisionProcess = "# Decision\nOutput directly." + cfg.CustomPrompt = "Chinese preference should not enter the system prompt." engine := NewStrategyEngine(&cfg) prompt := engine.BuildSystemPrompt(30, "balanced") diff --git a/kernel/formatter.go b/kernel/formatter.go index ae197217..72845dd3 100644 --- a/kernel/formatter.go +++ b/kernel/formatter.go @@ -105,7 +105,7 @@ func formatContextData(ctx *Context, lang Language) string { // formatHeaderZH formats header information (Chinese) func formatHeaderZH(ctx *Context) string { - return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n", + return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Cycle: #%d | Runtime: %d minutes\n\n", ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes) } @@ -114,18 +114,18 @@ func formatAccountZH(ctx *Context) string { acc := ctx.Account var sb strings.Builder - sb.WriteString("## 账户状态\n\n") - sb.WriteString(fmt.Sprintf("总权益: %.2f USDT | ", acc.TotalEquity)) - sb.WriteString(fmt.Sprintf("可用余额: %.2f USDT (%.1f%%) | ", acc.AvailableBalance, (acc.AvailableBalance/acc.TotalEquity)*100)) - sb.WriteString(fmt.Sprintf("总盈亏: %+.2f%% | ", acc.TotalPnLPct)) - sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct)) - sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount)) + sb.WriteString("## Account Status\n\n") + sb.WriteString(fmt.Sprintf("Total Equity: %.2f USDT | ", acc.TotalEquity)) + sb.WriteString(fmt.Sprintf("Available Balance: %.2f USDT (%.1f%%) | ", acc.AvailableBalance, (acc.AvailableBalance/acc.TotalEquity)*100)) + sb.WriteString(fmt.Sprintf("Total PnL: %+.2f%% | ", acc.TotalPnLPct)) + sb.WriteString(fmt.Sprintf("Margin Usage: %.1f%% | ", acc.MarginUsedPct)) + sb.WriteString(fmt.Sprintf("Position Count: %d\n\n", acc.PositionCount)) // Add risk warnings if acc.MarginUsedPct > 70 { - sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n") + sb.WriteString("⚠️ **Risk Warning**: Margin usage > 70%, in a high-risk state!\n\n") } else if acc.MarginUsedPct > 50 { - sb.WriteString("⚠️ **风险提示**: 保证金使用率 > 50%,建议谨慎开仓\n\n") + sb.WriteString("⚠️ **Risk Notice**: Margin usage > 50%, open positions with caution\n\n") } return sb.String() @@ -134,7 +134,7 @@ func formatAccountZH(ctx *Context) string { // formatTradingStatsZH formats historical trading statistics (Chinese) func formatTradingStatsZH(stats *TradingStats) string { var sb strings.Builder - sb.WriteString("## 历史交易统计\n\n") + sb.WriteString("## Historical Trading Statistics\n\n") // Win/loss ratio calculation var winLossRatio float64 @@ -143,49 +143,49 @@ func formatTradingStatsZH(stats *TradingStats) string { } // Metric definitions (focusing on core metrics, excluding win rate) - sb.WriteString("**指标说明**:\n") - sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n") - sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n") - sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n") - sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n") + sb.WriteString("**Metric Definitions**:\n") + sb.WriteString("- Profit Factor: Total Profit ÷ Total Loss (>1 means profitable, >1.5 good, >2 excellent)\n") + sb.WriteString("- Sharpe Ratio: (Avg Return - Risk-free Return) ÷ Std Dev of Returns (>1 good, >2 excellent)\n") + sb.WriteString("- Win/Loss Ratio: Avg Win ÷ Avg Loss (>1.5 good, >2 excellent)\n") + sb.WriteString("- Max Drawdown: Largest decline of the equity curve from peak to trough (<20% is low risk)\n\n") // Data values - sb.WriteString("**当前数据**:\n") - sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades)) - sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor)) - sb.WriteString(fmt.Sprintf("- 夏普比率: %.2f\n", stats.SharpeRatio)) - sb.WriteString(fmt.Sprintf("- 盈亏比: %.2f\n", winLossRatio)) - sb.WriteString(fmt.Sprintf("- 总盈亏: %+.2f USDT\n", stats.TotalPnL)) - sb.WriteString(fmt.Sprintf("- 平均盈利: +%.2f USDT\n", stats.AvgWin)) - sb.WriteString(fmt.Sprintf("- 平均亏损: -%.2f USDT\n", stats.AvgLoss)) - sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct)) + sb.WriteString("**Current Data**:\n") + sb.WriteString(fmt.Sprintf("- Total Trades: %d\n", stats.TotalTrades)) + sb.WriteString(fmt.Sprintf("- Profit Factor: %.2f\n", stats.ProfitFactor)) + sb.WriteString(fmt.Sprintf("- Sharpe Ratio: %.2f\n", stats.SharpeRatio)) + sb.WriteString(fmt.Sprintf("- Win/Loss Ratio: %.2f\n", winLossRatio)) + sb.WriteString(fmt.Sprintf("- Total PnL: %+.2f USDT\n", stats.TotalPnL)) + sb.WriteString(fmt.Sprintf("- Avg Win: +%.2f USDT\n", stats.AvgWin)) + sb.WriteString(fmt.Sprintf("- Avg Loss: -%.2f USDT\n", stats.AvgLoss)) + sb.WriteString(fmt.Sprintf("- Max Drawdown: %.1f%%\n\n", stats.MaxDrawdownPct)) // Comprehensive analysis and decision guidance - sb.WriteString("**决策参考**:\n") + sb.WriteString("**Decision Reference**:\n") // Provide specific recommendations based on statistics if stats.TotalTrades < 10 { - sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n") + sb.WriteString("- Small sample size (<10 trades), statistics have limited reference value\n") } if stats.ProfitFactor >= 1.5 && stats.SharpeRatio >= 1 { - sb.WriteString("- 📈 表现良好: 可以维持当前策略风格\n") + sb.WriteString("- 📈 Good performance: you can keep the current strategy style\n") } else if stats.ProfitFactor >= 1.0 { - sb.WriteString("- 📊 表现正常: 策略可行但有优化空间\n") + sb.WriteString("- 📊 Normal performance: strategy is viable but has room for optimization\n") } if stats.ProfitFactor < 1.0 { - sb.WriteString("- ⚠️ 盈利因子<1: 亏损大于盈利,需要提高盈亏比,优化止盈止损\n") + sb.WriteString("- ⚠️ Profit Factor <1: losses exceed profits, improve win/loss ratio and optimize stops/targets\n") } if winLossRatio > 0 && winLossRatio < 1.5 { - sb.WriteString("- ⚠️ 盈亏比偏低: 建议让利润奔跑,提高止盈目标\n") + sb.WriteString("- ⚠️ Low win/loss ratio: let profits run and raise take-profit targets\n") } if stats.MaxDrawdownPct > 30 { - sb.WriteString("- ⚠️ 最大回撤过高: 建议降低仓位大小控制风险\n") + sb.WriteString("- ⚠️ Max drawdown too high: reduce position size to control risk\n") } else if stats.MaxDrawdownPct < 10 { - sb.WriteString("- ✅ 回撤控制良好: 风险管理有效\n") + sb.WriteString("- ✅ Drawdown well controlled: risk management is effective\n") } sb.WriteString("\n") @@ -195,16 +195,16 @@ func formatTradingStatsZH(stats *TradingStats) string { // formatRecentTradesZH formats recent trades (Chinese) func formatRecentTradesZH(orders []RecentOrder) string { var sb strings.Builder - sb.WriteString("## 最近完成的交易\n\n") + sb.WriteString("## Recently Closed Trades\n\n") for i, order := range orders { // Determine profit or loss - profitOrLoss := "盈利" + profitOrLoss := "Profit" if order.RealizedPnL < 0 { - profitOrLoss = "亏损" + profitOrLoss = "Loss" } - sb.WriteString(fmt.Sprintf("%d. %s %s | 进场 %.4f 出场 %.4f | %s: %+.2f USDT (%+.2f%%) | %s → %s (%s)\n", + sb.WriteString(fmt.Sprintf("%d. %s %s | Entry %.4f Exit %.4f | %s: %+.2f USDT (%+.2f%%) | %s → %s (%s)\n", i+1, order.Symbol, order.Side, @@ -226,37 +226,37 @@ func formatRecentTradesZH(orders []RecentOrder) string { // formatCurrentPositionsZH formats current positions (Chinese) func formatCurrentPositionsZH(ctx *Context) string { var sb strings.Builder - sb.WriteString("## 当前持仓\n\n") + sb.WriteString("## Current Positions\n\n") for i, pos := range ctx.Positions { // Calculate drawdown drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side))) - sb.WriteString(fmt.Sprintf("进场 %.4f 当前 %.4f | ", pos.EntryPrice, pos.MarkPrice)) - sb.WriteString(fmt.Sprintf("数量 %.4f | ", pos.Quantity)) - sb.WriteString(fmt.Sprintf("仓位价值 %.2f USDT | ", pos.Quantity*pos.MarkPrice)) - sb.WriteString(fmt.Sprintf("盈亏 %+.2f%% | ", pos.UnrealizedPnLPct)) - sb.WriteString(fmt.Sprintf("盈亏金额 %+.2f USDT | ", pos.UnrealizedPnL)) - sb.WriteString(fmt.Sprintf("峰值盈亏 %.2f%% | ", pos.PeakPnLPct)) - sb.WriteString(fmt.Sprintf("杠杆 %dx | ", pos.Leverage)) - sb.WriteString(fmt.Sprintf("保证金 %.0f USDT | ", pos.MarginUsed)) - sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice)) + sb.WriteString(fmt.Sprintf("Entry %.4f Current %.4f | ", pos.EntryPrice, pos.MarkPrice)) + sb.WriteString(fmt.Sprintf("Quantity %.4f | ", pos.Quantity)) + sb.WriteString(fmt.Sprintf("Position Value %.2f USDT | ", pos.Quantity*pos.MarkPrice)) + sb.WriteString(fmt.Sprintf("PnL %+.2f%% | ", pos.UnrealizedPnLPct)) + sb.WriteString(fmt.Sprintf("PnL Amount %+.2f USDT | ", pos.UnrealizedPnL)) + sb.WriteString(fmt.Sprintf("Peak PnL %.2f%% | ", pos.PeakPnLPct)) + sb.WriteString(fmt.Sprintf("Leverage %dx | ", pos.Leverage)) + sb.WriteString(fmt.Sprintf("Margin %.0f USDT | ", pos.MarginUsed)) + sb.WriteString(fmt.Sprintf("Liq Price %.4f\n", pos.LiquidationPrice)) // Add analysis hints if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 { - sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n", + sb.WriteString(fmt.Sprintf(" ⚠️ **Take-Profit Hint**: Current PnL retraced from peak %.2f%% to %.2f%%, drawdown %.2f%%, consider taking profit\n", pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100)) } if pos.UnrealizedPnLPct < -4.0 { - sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n") + sb.WriteString(" ⚠️ **Stop-Loss Hint**: Loss approaching the -5% stop-loss line, consider stopping out\n") } // Show current price (if market data available) if ctx.MarketDataMap != nil { if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok { - sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice)) + sb.WriteString(fmt.Sprintf(" 📈 Current Price: %.4f\n", mdata.CurrentPrice)) } } @@ -269,7 +269,7 @@ func formatCurrentPositionsZH(ctx *Context) string { // formatCandidateCoinsZH formats candidate coins (Chinese) func formatCandidateCoinsZH(ctx *Context) string { var sb strings.Builder - sb.WriteString("## 候选币种\n\n") + sb.WriteString("## Candidate Coins\n\n") for i, coin := range ctx.CandidateCoins { sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol)) @@ -277,7 +277,7 @@ func formatCandidateCoinsZH(ctx *Context) string { // Current price if ctx.MarketDataMap != nil { if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok { - sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice)) + sb.WriteString(fmt.Sprintf("Current Price: %.4f\n\n", mdata.CurrentPrice)) // Kline data (multi-timeframe) if mdata.TimeframeData != nil { @@ -289,7 +289,7 @@ func formatCandidateCoinsZH(ctx *Context) string { // OI data (if available) if ctx.OITopDataMap != nil { if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok { - sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n", + sb.WriteString(fmt.Sprintf("**OI Change**: OI Rank #%d | Change %+.2f%% (%+.2fM USDT) | Price Change %+.2f%%\n\n", oiData.Rank, oiData.OIDeltaPercent, oiData.OIDeltaValue/1_000_000, @@ -297,17 +297,17 @@ func formatCandidateCoinsZH(ctx *Context) string { )) // OI interpretation - oiChange := "增加" + oiChange := "increase" if oiData.OIDeltaPercent < 0 { - oiChange = "减少" + oiChange = "decrease" } - priceChange := "上涨" + priceChange := "up" if oiData.PriceDeltaPercent < 0 { - priceChange = "下跌" + priceChange = "down" } interpretation := getOIInterpretationZH(oiChange, priceChange) - sb.WriteString(fmt.Sprintf("**市场解读**: %s\n\n", interpretation)) + sb.WriteString(fmt.Sprintf("**Market Interpretation**: %s\n\n", interpretation)) } } } @@ -321,9 +321,9 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD for _, tf := range timeframes { if data, ok := tfData[tf]; ok && len(data.Klines) > 0 { - sb.WriteString(fmt.Sprintf("#### %s 时间框架 (从旧到新)\n\n", tf)) + sb.WriteString(fmt.Sprintf("#### %s Timeframe (oldest to newest)\n\n", tf)) sb.WriteString("```\n") - sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n") + sb.WriteString("Time(UTC) Open High Low Close Volume\n") // Only show the latest 30 klines startIdx := 0 @@ -346,7 +346,7 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD // Mark the last kline if len(data.Klines) > 0 { - sb.WriteString(" <- 当前\n") + sb.WriteString(" <- current\n") } sb.WriteString("```\n\n") @@ -356,14 +356,13 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD return sb.String() } - // getOIInterpretationZH returns OI change interpretation (Chinese) func getOIInterpretationZH(oiChange, priceChange string) string { - if oiChange == "增加" && priceChange == "上涨" { + if oiChange == "increase" && priceChange == "up" { return OIInterpretation.OIUp_PriceUp.ZH - } else if oiChange == "增加" && priceChange == "下跌" { + } else if oiChange == "increase" && priceChange == "down" { return OIInterpretation.OIUp_PriceDown.ZH - } else if oiChange == "减少" && priceChange == "上涨" { + } else if oiChange == "decrease" && priceChange == "up" { return OIInterpretation.OIDown_PriceUp.ZH } else { return OIInterpretation.OIDown_PriceDown.ZH @@ -621,7 +620,6 @@ func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesD return sb.String() } - // getOIInterpretationEN returns OI change interpretation (English) func getOIInterpretationEN(oiChange, priceChange string) string { if oiChange == "increase" && priceChange == "up" { diff --git a/kernel/grid_engine.go b/kernel/grid_engine.go index f376e259..595d1a71 100644 --- a/kernel/grid_engine.go +++ b/kernel/grid_engine.go @@ -17,24 +17,24 @@ import ( // GridLevelInfo represents a single grid level's current state type GridLevelInfo struct { - Index int `json:"index"` // Level index (0 = lowest) - Price float64 `json:"price"` // Target price for this level - State string `json:"state"` // "empty", "pending", "filled" - Side string `json:"side"` // "buy" or "sell" - OrderID string `json:"order_id"` // Current order ID (if pending) - OrderQuantity float64 `json:"order_quantity"` // Order quantity - PositionSize float64 `json:"position_size"` // Position size (if filled) - PositionEntry float64 `json:"position_entry"` // Entry price (if filled) - AllocatedUSD float64 `json:"allocated_usd"` // USD allocated to this level - UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized P&L (if filled) + Index int `json:"index"` // Level index (0 = lowest) + Price float64 `json:"price"` // Target price for this level + State string `json:"state"` // "empty", "pending", "filled" + Side string `json:"side"` // "buy" or "sell" + OrderID string `json:"order_id"` // Current order ID (if pending) + OrderQuantity float64 `json:"order_quantity"` // Order quantity + PositionSize float64 `json:"position_size"` // Position size (if filled) + PositionEntry float64 `json:"position_entry"` // Entry price (if filled) + AllocatedUSD float64 `json:"allocated_usd"` // USD allocated to this level + UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized P&L (if filled) } // GridContext contains all information needed for AI grid decision making type GridContext struct { // Basic info - Symbol string `json:"symbol"` - CurrentTime string `json:"current_time"` - CurrentPrice float64 `json:"current_price"` + Symbol string `json:"symbol"` + CurrentTime string `json:"current_time"` + CurrentPrice float64 `json:"current_price"` // Grid configuration GridCount int `json:"grid_count"` @@ -52,22 +52,22 @@ type GridContext struct { IsPaused bool `json:"is_paused"` // Market data - ATR14 float64 `json:"atr14"` - BollingerUpper float64 `json:"bollinger_upper"` + ATR14 float64 `json:"atr14"` + BollingerUpper float64 `json:"bollinger_upper"` BollingerMiddle float64 `json:"bollinger_middle"` - BollingerLower float64 `json:"bollinger_lower"` - BollingerWidth float64 `json:"bollinger_width"` // Percentage - EMA20 float64 `json:"ema20"` - EMA50 float64 `json:"ema50"` - EMADistance float64 `json:"ema_distance"` // Percentage - RSI14 float64 `json:"rsi14"` - MACD float64 `json:"macd"` - MACDSignal float64 `json:"macd_signal"` - MACDHistogram float64 `json:"macd_histogram"` - FundingRate float64 `json:"funding_rate"` - Volume24h float64 `json:"volume_24h"` - PriceChange1h float64 `json:"price_change_1h"` - PriceChange4h float64 `json:"price_change_4h"` + BollingerLower float64 `json:"bollinger_lower"` + BollingerWidth float64 `json:"bollinger_width"` // Percentage + EMA20 float64 `json:"ema20"` + EMA50 float64 `json:"ema50"` + EMADistance float64 `json:"ema_distance"` // Percentage + RSI14 float64 `json:"rsi14"` + MACD float64 `json:"macd"` + MACDSignal float64 `json:"macd_signal"` + MACDHistogram float64 `json:"macd_histogram"` + FundingRate float64 `json:"funding_rate"` + Volume24h float64 `json:"volume_24h"` + PriceChange1h float64 `json:"price_change_1h"` + PriceChange4h float64 `json:"price_change_4h"` // Account info TotalEquity float64 `json:"total_equity"` @@ -102,53 +102,53 @@ func BuildGridSystemPrompt(config *store.GridStrategyConfig, lang string) string } func buildGridSystemPromptZh(config *store.GridStrategyConfig) string { - return fmt.Sprintf(`# 你是一个专业的网格交易AI + return fmt.Sprintf(`# You are a Professional Grid Trading AI -## 角色定义 -你是一个经验丰富的网格交易专家,负责管理 %s 的网格交易策略。你的任务是: -1. 判断当前市场状态(震荡/趋势/高波动) -2. 决定是否需要调整网格或暂停交易 -3. 管理每个网格层级的订单 +## Role Definition +You are an experienced grid trading expert responsible for managing the grid trading strategy for %s. Your tasks are: +1. Determine the current market state (ranging/trending/high volatility) +2. Decide whether to adjust the grid or pause trading +3. Manage orders at each grid level -## 网格配置 -- 交易对: %s -- 网格层数: %d -- 总投资: %.2f USDT -- 杠杆: %dx -- 价格分布: %s +## Grid Configuration +- Trading Pair: %s +- Grid Levels: %d +- Total Investment: %.2f USDT +- Leverage: %dx +- Price Distribution: %s -## 决策规则 +## Decision Rules -### 市场状态判断 -- **震荡市场** (适合网格): 布林带宽度 < 3%%, EMA20/50 距离 < 1%%, 价格在布林带中轨附近 -- **趋势市场** (暂停网格): 布林带宽度 > 4%%, EMA20/50 距离 > 2%%, 价格持续突破布林带 -- **高波动市场** (谨慎): ATR异常放大, 价格剧烈波动 +### Market State Judgment +- **Ranging Market** (suitable for grid): Bollinger band width < 3%%, EMA20/50 distance < 1%%, price near the Bollinger middle band +- **Trending Market** (pause grid): Bollinger band width > 4%%, EMA20/50 distance > 2%%, price continuously breaking out of the Bollinger bands +- **High Volatility Market** (caution): abnormally expanding ATR, sharp price swings -### 可执行的操作 -- place_buy_limit: 在指定价格下买入限价单 -- place_sell_limit: 在指定价格下卖出限价单 -- cancel_order: 取消指定订单 -- cancel_all_orders: 取消所有订单 -- pause_grid: 暂停网格交易(趋势市场时) -- resume_grid: 恢复网格交易(震荡市场时) -- adjust_grid: 调整网格边界 -- hold: 保持当前状态不操作 +### Available Actions +- place_buy_limit: place a buy limit order at the specified price +- place_sell_limit: place a sell limit order at the specified price +- cancel_order: cancel a specified order +- cancel_all_orders: cancel all orders +- pause_grid: pause grid trading (in a trending market) +- resume_grid: resume grid trading (in a ranging market) +- adjust_grid: adjust the grid boundaries +- hold: keep the current state, take no action -## 输出格式 -输出JSON数组,每个决策包含: -- symbol: 交易对 -- action: 操作类型 -- price: 价格(限价单用) -- quantity: 数量 -- level_index: 网格层级索引 -- order_id: 订单ID(取消订单用) -- confidence: 置信度 0-100 -- reasoning: 决策理由 +## Output Format +Output a JSON array; each decision contains: +- symbol: trading pair +- action: action type +- price: price (for limit orders) +- quantity: quantity +- level_index: grid level index +- order_id: order ID (for canceling orders) +- confidence: confidence 0-100 +- reasoning: decision reasoning -示例: +Example: [ - {"symbol": "BTCUSDT", "action": "place_buy_limit", "price": 94000, "quantity": 0.01, "level_index": 2, "confidence": 85, "reasoning": "第2层价格接近,下买单"}, - {"symbol": "BTCUSDT", "action": "hold", "confidence": 90, "reasoning": "市场震荡,保持当前网格"} + {"symbol": "BTCUSDT", "action": "place_buy_limit", "price": 94000, "quantity": 0.01, "level_index": 2, "confidence": 85, "reasoning": "Level 2 price is near, place a buy order"}, + {"symbol": "BTCUSDT", "action": "hold", "confidence": 90, "reasoning": "Market is ranging, keep the current grid"} ] `, config.Symbol, config.Symbol, config.GridCount, config.TotalInvestment, config.Leverage, config.Distribution) } @@ -216,26 +216,26 @@ func BuildGridUserPrompt(ctx *GridContext, lang string) string { func buildGridUserPromptZh(ctx *GridContext) string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("## 当前时间: %s\n\n", ctx.CurrentTime)) + sb.WriteString(fmt.Sprintf("## Current Time: %s\n\n", ctx.CurrentTime)) // Market data section - sb.WriteString("## 市场数据\n") - sb.WriteString(fmt.Sprintf("- 当前价格: $%.2f\n", ctx.CurrentPrice)) - sb.WriteString(fmt.Sprintf("- 1小时涨跌: %.2f%%\n", ctx.PriceChange1h)) - sb.WriteString(fmt.Sprintf("- 4小时涨跌: %.2f%%\n", ctx.PriceChange4h)) + sb.WriteString("## Market Data\n") + sb.WriteString(fmt.Sprintf("- Current Price: $%.2f\n", ctx.CurrentPrice)) + sb.WriteString(fmt.Sprintf("- 1h Change: %.2f%%\n", ctx.PriceChange1h)) + sb.WriteString(fmt.Sprintf("- 4h Change: %.2f%%\n", ctx.PriceChange4h)) sb.WriteString(fmt.Sprintf("- ATR14: $%.2f (%.2f%%)\n", ctx.ATR14, ctx.ATR14/ctx.CurrentPrice*100)) - sb.WriteString(fmt.Sprintf("- 布林带: 上轨 $%.2f, 中轨 $%.2f, 下轨 $%.2f\n", ctx.BollingerUpper, ctx.BollingerMiddle, ctx.BollingerLower)) - sb.WriteString(fmt.Sprintf("- 布林带宽度: %.2f%%\n", ctx.BollingerWidth)) - sb.WriteString(fmt.Sprintf("- EMA20: $%.2f, EMA50: $%.2f, 距离: %.2f%%\n", ctx.EMA20, ctx.EMA50, ctx.EMADistance)) + sb.WriteString(fmt.Sprintf("- Bollinger Bands: Upper $%.2f, Middle $%.2f, Lower $%.2f\n", ctx.BollingerUpper, ctx.BollingerMiddle, ctx.BollingerLower)) + sb.WriteString(fmt.Sprintf("- Bollinger Width: %.2f%%\n", ctx.BollingerWidth)) + sb.WriteString(fmt.Sprintf("- EMA20: $%.2f, EMA50: $%.2f, Distance: %.2f%%\n", ctx.EMA20, ctx.EMA50, ctx.EMADistance)) sb.WriteString(fmt.Sprintf("- RSI14: %.1f\n", ctx.RSI14)) sb.WriteString(fmt.Sprintf("- MACD: %.4f, Signal: %.4f, Histogram: %.4f\n", ctx.MACD, ctx.MACDSignal, ctx.MACDHistogram)) - sb.WriteString(fmt.Sprintf("- 资金费率: %.4f%%\n", ctx.FundingRate*100)) + sb.WriteString(fmt.Sprintf("- Funding Rate: %.4f%%\n", ctx.FundingRate*100)) sb.WriteString("\n") // Box Indicator Section if ctx.BoxData != nil { - sb.WriteString("## 箱体指标 (唐奇安通道)\n\n") - sb.WriteString("| 箱体级别 | 上轨 | 下轨 | 宽度 |\n") + sb.WriteString("## Box Indicator (Donchian Channel)\n\n") + sb.WriteString("| Box Level | Upper | Lower | Width |\n") sb.WriteString("|----------|------|------|------|\n") shortWidth := 0.0 @@ -248,59 +248,59 @@ func buildGridUserPromptZh(ctx *GridContext) string { longWidth = (ctx.BoxData.LongUpper - ctx.BoxData.LongLower) / ctx.BoxData.CurrentPrice * 100 } - sb.WriteString(fmt.Sprintf("| 短期 (3天) | %.2f | %.2f | %.2f%% |\n", + sb.WriteString(fmt.Sprintf("| Short (3d) | %.2f | %.2f | %.2f%% |\n", ctx.BoxData.ShortUpper, ctx.BoxData.ShortLower, shortWidth)) - sb.WriteString(fmt.Sprintf("| 中期 (10天) | %.2f | %.2f | %.2f%% |\n", + sb.WriteString(fmt.Sprintf("| Mid (10d) | %.2f | %.2f | %.2f%% |\n", ctx.BoxData.MidUpper, ctx.BoxData.MidLower, midWidth)) - sb.WriteString(fmt.Sprintf("| 长期 (21天) | %.2f | %.2f | %.2f%% |\n", + sb.WriteString(fmt.Sprintf("| Long (21d) | %.2f | %.2f | %.2f%% |\n", ctx.BoxData.LongUpper, ctx.BoxData.LongLower, longWidth)) - sb.WriteString(fmt.Sprintf("\n当前价格: %.2f\n", ctx.BoxData.CurrentPrice)) + sb.WriteString(fmt.Sprintf("\nCurrent Price: %.2f\n", ctx.BoxData.CurrentPrice)) // Check position relative to boxes price := ctx.BoxData.CurrentPrice if price > ctx.BoxData.LongUpper || price < ctx.BoxData.LongLower { - sb.WriteString("⚠️ 突破: 价格突破长期箱体!\n") + sb.WriteString("⚠️ Breakout: price broke out of the long-term box!\n") } else if price > ctx.BoxData.MidUpper || price < ctx.BoxData.MidLower { - sb.WriteString("⚠️ 警告: 价格接近长期箱体边界\n") + sb.WriteString("⚠️ Warning: price is approaching the long-term box boundary\n") } sb.WriteString("\n") } // Account section - sb.WriteString("## 账户状态\n") - sb.WriteString(fmt.Sprintf("- 总权益: $%.2f\n", ctx.TotalEquity)) - sb.WriteString(fmt.Sprintf("- 可用余额: $%.2f\n", ctx.AvailableBalance)) - sb.WriteString(fmt.Sprintf("- 当前持仓: %.4f (净头寸)\n", ctx.CurrentPosition)) - sb.WriteString(fmt.Sprintf("- 未实现盈亏: $%.2f\n", ctx.UnrealizedPnL)) + sb.WriteString("## Account Status\n") + sb.WriteString(fmt.Sprintf("- Total Equity: $%.2f\n", ctx.TotalEquity)) + sb.WriteString(fmt.Sprintf("- Available Balance: $%.2f\n", ctx.AvailableBalance)) + sb.WriteString(fmt.Sprintf("- Current Position: %.4f (net position)\n", ctx.CurrentPosition)) + sb.WriteString(fmt.Sprintf("- Unrealized PnL: $%.2f\n", ctx.UnrealizedPnL)) sb.WriteString("\n") // Grid state section - sb.WriteString("## 网格状态\n") - sb.WriteString(fmt.Sprintf("- 网格范围: $%.2f - $%.2f\n", ctx.LowerPrice, ctx.UpperPrice)) - sb.WriteString(fmt.Sprintf("- 网格间距: $%.2f\n", ctx.GridSpacing)) - sb.WriteString(fmt.Sprintf("- 活跃订单数: %d\n", ctx.ActiveOrderCount)) - sb.WriteString(fmt.Sprintf("- 已成交层数: %d\n", ctx.FilledLevelCount)) - sb.WriteString(fmt.Sprintf("- 网格已暂停: %v\n", ctx.IsPaused)) + sb.WriteString("## Grid State\n") + sb.WriteString(fmt.Sprintf("- Grid Range: $%.2f - $%.2f\n", ctx.LowerPrice, ctx.UpperPrice)) + sb.WriteString(fmt.Sprintf("- Grid Spacing: $%.2f\n", ctx.GridSpacing)) + sb.WriteString(fmt.Sprintf("- Active Orders: %d\n", ctx.ActiveOrderCount)) + sb.WriteString(fmt.Sprintf("- Filled Levels: %d\n", ctx.FilledLevelCount)) + sb.WriteString(fmt.Sprintf("- Grid Paused: %v\n", ctx.IsPaused)) if ctx.CurrentDirection != "" { directionDescZh := map[string]string{ - "neutral": "中性 (50%买+50%卖)", - "long": "做多 (100%买)", - "short": "做空 (100%卖)", - "long_bias": "偏多 (70%买+30%卖)", - "short_bias": "偏空 (30%买+70%卖)", + "neutral": "Neutral (50% buy + 50% sell)", + "long": "Long (100% buy)", + "short": "Short (100% sell)", + "long_bias": "Long bias (70% buy + 30% sell)", + "short_bias": "Short bias (30% buy + 70% sell)", } desc := directionDescZh[ctx.CurrentDirection] if desc == "" { desc = ctx.CurrentDirection } - sb.WriteString(fmt.Sprintf("- 网格方向: %s\n", desc)) + sb.WriteString(fmt.Sprintf("- Grid Direction: %s\n", desc)) } sb.WriteString("\n") // Grid levels detail - sb.WriteString("## 网格层级详情\n") - sb.WriteString("| 层级 | 价格 | 状态 | 方向 | 订单数量 | 持仓数量 | 未实现盈亏 |\n") + sb.WriteString("## Grid Level Details\n") + sb.WriteString("| Level | Price | State | Side | Order Qty | Position Qty | Unrealized PnL |\n") sb.WriteString("|------|------|------|------|----------|----------|------------|\n") for _, level := range ctx.Levels { sb.WriteString(fmt.Sprintf("| %d | $%.2f | %s | %s | %.4f | %.4f | $%.2f |\n", @@ -310,16 +310,16 @@ func buildGridUserPromptZh(ctx *GridContext) string { sb.WriteString("\n") // Performance section - sb.WriteString("## 绩效统计\n") - sb.WriteString(fmt.Sprintf("- 总利润: $%.2f\n", ctx.TotalProfit)) - sb.WriteString(fmt.Sprintf("- 总交易次数: %d\n", ctx.TotalTrades)) - sb.WriteString(fmt.Sprintf("- 胜率: %.1f%%\n", float64(ctx.WinningTrades)/float64(max(ctx.TotalTrades, 1))*100)) - sb.WriteString(fmt.Sprintf("- 最大回撤: %.2f%%\n", ctx.MaxDrawdown)) - sb.WriteString(fmt.Sprintf("- 今日盈亏: $%.2f\n", ctx.DailyPnL)) + sb.WriteString("## Performance Statistics\n") + sb.WriteString(fmt.Sprintf("- Total Profit: $%.2f\n", ctx.TotalProfit)) + sb.WriteString(fmt.Sprintf("- Total Trades: %d\n", ctx.TotalTrades)) + sb.WriteString(fmt.Sprintf("- Win Rate: %.1f%%\n", float64(ctx.WinningTrades)/float64(max(ctx.TotalTrades, 1))*100)) + sb.WriteString(fmt.Sprintf("- Max Drawdown: %.2f%%\n", ctx.MaxDrawdown)) + sb.WriteString(fmt.Sprintf("- Today's PnL: $%.2f\n", ctx.DailyPnL)) sb.WriteString("\n") - sb.WriteString("## 请分析以上数据,做出网格交易决策\n") - sb.WriteString("输出JSON数组格式的决策列表。\n") + sb.WriteString("## Analyze the data above and make grid trading decisions\n") + sb.WriteString("Output the decision list as a JSON array.\n") return sb.String() } @@ -541,9 +541,9 @@ func isValidGridAction(action string) bool { "adjust_grid": true, "hold": true, // Also support standard actions for compatibility - "open_long": true, - "open_short": true, - "close_long": true, + "open_long": true, + "open_short": true, + "close_long": true, "close_short": true, } return validActions[action] diff --git a/kernel/prompt_builder.go b/kernel/prompt_builder.go index 60554bcc..e306bc4d 100644 --- a/kernel/prompt_builder.go +++ b/kernel/prompt_builder.go @@ -41,43 +41,43 @@ func (pb *PromptBuilder) BuildUserPrompt(ctx *Context) string { return formattedData + pb.getDecisionRequirementsEN() } -// ========== Chinese Prompts ========== +// ========== Chinese Prompts (translated to English) ========== func (pb *PromptBuilder) buildSystemPromptZH() string { - return `你是一个专业的量化交易AI助手,负责分析市场数据并做出交易决策。 + return `You are a professional quantitative trading AI assistant, responsible for analyzing market data and making trading decisions. -## 你的任务 +## Your Tasks -1. **分析账户状态**: 评估当前风险水平、保证金使用率、持仓情况 -2. **分析当前持仓**: 判断是否需要止盈、止损、加仓或持有 -3. **分析候选币种**: 评估新的交易机会,结合技术分析和资金流向 -4. **做出决策**: 输出明确的交易决策,包含详细的推理过程 +1. **Analyze account status**: Evaluate current risk level, margin usage, and position status +2. **Analyze current positions**: Decide whether to take profit, stop loss, add to position, or hold +3. **Analyze candidate symbols**: Evaluate new trading opportunities, combining technical analysis and capital flow +4. **Make decisions**: Output clear trading decisions with detailed reasoning -## 决策原则 +## Decision Principles -### 风险优先 -- 保证金使用率不得超过30% -- 单个持仓亏损达到-5%必须止损 -- 优先保护资本,再考虑盈利 +### Risk First +- Margin usage must not exceed 30% +- A single position losing -5% must be stopped out +- Protect capital first, then consider profit -### 跟踪止盈 -- 当持仓盈亏从峰值回撤30%时,考虑部分或全部止盈 -- 例如:Peak PnL +5%,Current PnL +3.5% → 回撤了30%,应该止盈 +### Trailing Take-Profit +- When position PnL retraces 30% from its peak, consider partial or full take-profit +- For example: Peak PnL +5%, Current PnL +3.5% -> retraced 30%, should take profit -### 顺势交易 -- 只在多个时间框架趋势一致时进场 -- 结合持仓量(OI)变化判断资金流向真实性 -- OI增加+价格上涨 = 强多头趋势 -- OI减少+价格上涨 = 空头平仓(可能反转) +### Trend Following +- Enter only when multiple timeframes' trends agree +- Use open interest (OI) change to judge the authenticity of capital flow +- OI up + price up = strong bullish trend +- OI down + price up = short covering (possible reversal) -### 分批操作 -- 分批建仓:第一次开仓不超过目标仓位的50% -- 分批止盈:盈利3%平33%,盈利5%平50%,盈利8%全平 -- 只在盈利仓位上加仓,永远不要追亏损 +### Scaling +- Scale in: the first open should not exceed 50% of the target position +- Scale out: at +3% profit close 33%, at +5% close 50%, at +8% close all +- Only add to profitable positions, never chase losses -## 输出格式要求 +## Output Format Requirements -**必须**使用以下JSON格式输出决策: +You **must** output decisions in the following JSON format: ` + "```json" + ` [ @@ -89,37 +89,37 @@ func (pb *PromptBuilder) buildSystemPromptZH() string { "stop_loss": 42000, "take_profit": 48000, "confidence": 85, - "reasoning": "详细的推理过程,说明为什么做出这个决策" + "reasoning": "Detailed reasoning explaining why this decision was made" } ] ` + "```" + ` -### 字段说明 +### Field Descriptions -- **symbol**: 交易对(必需) -- **action**: 动作类型(必需) - - HOLD: 持有当前仓位 - - PARTIAL_CLOSE: 部分平仓 - - FULL_CLOSE: 全部平仓 - - ADD_POSITION: 在现有仓位上加仓 - - OPEN_NEW: 开设新仓位 - - WAIT: 等待,不采取任何行动 -- **leverage**: 杠杆倍数(开新仓时必需) -- **position_size_usd**: 仓位大小(USDT,开新仓时必需) -- **stop_loss**: 止损价格(开新仓时建议提供) -- **take_profit**: 止盈价格(开新仓时建议提供) -- **confidence**: 信心度(0-100) -- **reasoning**: 推理过程(必需,必须详细说明决策依据) +- **symbol**: trading pair (required) +- **action**: action type (required) + - HOLD: hold the current position + - PARTIAL_CLOSE: partially close the position + - FULL_CLOSE: fully close the position + - ADD_POSITION: add to an existing position + - OPEN_NEW: open a new position + - WAIT: wait, take no action +- **leverage**: leverage multiple (required when opening a new position) +- **position_size_usd**: position size (USDT, required when opening a new position) +- **stop_loss**: stop-loss price (recommended when opening a new position) +- **take_profit**: take-profit price (recommended when opening a new position) +- **confidence**: confidence level (0-100) +- **reasoning**: reasoning (required, must explain the decision basis in detail) -## 重要提醒 +## Important Reminders -1. **永远不要**混淆已实现盈亏和未实现盈亏 -2. **永远记得**考虑杠杆对盈亏的放大作用 -3. **永远关注**Peak PnL,这是判断止盈的关键指标 -4. **永远结合**持仓量(OI)变化来判断趋势真实性 -5. **永远遵守**风险管理规则,保护资本是第一位的 +1. **Never** confuse realized PnL with unrealized PnL +2. **Always remember** to account for leverage amplifying PnL +3. **Always watch** Peak PnL, the key metric for take-profit decisions +4. **Always combine** open interest (OI) change to judge trend authenticity +5. **Always follow** risk management rules; protecting capital comes first -现在,请仔细分析接下来提供的交易数据,并做出专业的决策。` +Now, carefully analyze the trading data provided next and make a professional decision.` } func (pb *PromptBuilder) getDecisionRequirementsZH() string { @@ -127,30 +127,30 @@ func (pb *PromptBuilder) getDecisionRequirementsZH() string { --- -## 📝 现在请做出决策 +## 📝 Now Make Your Decision -### 决策步骤 +### Decision Steps -1. **分析账户风险**: - - 当前保证金使用率是否在安全范围? - - 是否有足够资金开新仓? +1. **Analyze account risk**: + - Is the current margin usage within a safe range? + - Is there enough capital to open new positions? -2. **分析现有持仓**(如果有): - - 是否触发止损条件? - - 是否触发跟踪止盈条件? - - 是否适合加仓? +2. **Analyze existing positions** (if any): + - Are stop-loss conditions triggered? + - Are trailing take-profit conditions triggered? + - Is it suitable to add to the position? -3. **分析候选币种**(如果有): - - 技术形态是否符合进场条件? - - 持仓量变化是否支持趋势? - - 多个时间框架是否共振? +3. **Analyze candidate symbols** (if any): + - Does the technical pattern meet entry conditions? + - Does the open interest change support the trend? + - Do multiple timeframes resonate? -4. **输出决策**: - - 使用规定的JSON格式 - - 提供详细的推理过程 - - 给出明确的行动指令 +4. **Output the decision**: + - Use the specified JSON format + - Provide detailed reasoning + - Give clear action instructions -### 输出示例 +### Output Example ` + "```json" + ` [ @@ -158,7 +158,7 @@ func (pb *PromptBuilder) getDecisionRequirementsZH() string { "symbol": "PIPPINUSDT", "action": "PARTIAL_CLOSE", "confidence": 85, - "reasoning": "当前PnL +2.96%,接近历史峰值+2.99%(回撤仅0.03%)。建议部分平仓锁定利润,因为:1) 持仓时间仅11分钟,已获得3%收益;2) 5分钟K线显示价格接近短期阻力位;3) 成交量开始萎缩,上涨动能减弱。建议平仓50%,剩余仓位设置跟踪止盈在峰值回撤20%处。" + "reasoning": "Current PnL +2.96%, close to the all-time peak +2.99% (only 0.03% retracement). Recommend partial close to lock in profit because: 1) holding time is only 11 minutes with 3% gain already; 2) the 5-minute candle shows price near short-term resistance; 3) volume is starting to shrink and upward momentum is weakening. Recommend closing 50%, with the remaining position set to a trailing take-profit at 20% retracement from peak." }, { "symbol": "HUSDT", @@ -168,12 +168,12 @@ func (pb *PromptBuilder) getDecisionRequirementsZH() string { "stop_loss": 0.1560, "take_profit": 0.1720, "confidence": 75, - "reasoning": "HUSDT在5分钟时间框架突破关键阻力位0.1630,持仓量1小时内增加+1.57M (+0.89%),配合价格上涨+4.92%,符合'OI增加+价格上涨'的强多头模式。15分钟和1小时时间框架均呈现上涨趋势,多周期共振。建议开仓做多,止损设在突破点下方-5%,止盈目标+8%。" + "reasoning": "HUSDT broke the key resistance 0.1630 on the 5-minute timeframe, open interest increased +1.57M (+0.89%) within 1 hour, together with a price rise of +4.92%, matching the strong bullish 'OI up + price up' pattern. Both the 15-minute and 1-hour timeframes show an uptrend, multi-period resonance. Recommend opening long, with stop-loss set 5% below the breakout point and take-profit target +8%." } ] ` + "```" + ` -**请立即输出你的决策(JSON格式)**:` +**Output your decision immediately (JSON format)**:` } // ========== English Prompts ========== diff --git a/kernel/prompt_builder_test.go b/kernel/prompt_builder_test.go index ad369548..9019d838 100644 --- a/kernel/prompt_builder_test.go +++ b/kernel/prompt_builder_test.go @@ -6,7 +6,7 @@ import ( "time" ) -// TestPromptBuilder 测试提示词构建器 +// TestPromptBuilder tests the prompt builder func TestPromptBuilder(t *testing.T) { t.Run("NewPromptBuilder", func(t *testing.T) { builderZH := NewPromptBuilder(LangChinese) @@ -31,17 +31,17 @@ func TestPromptBuilder(t *testing.T) { t.Fatal("System prompt is empty") } - // 验证包含关键内容 + // Verify it contains key content mustContain := []string{ - "量化交易AI助手", - "分析账户状态", - "分析当前持仓", - "分析候选币种", - "做出决策", - "风险优先", - "跟踪止盈", - "顺势交易", - "分批操作", + "quantitative trading AI assistant", + "Analyze account status", + "Analyze current positions", + "Analyze candidate symbols", + "Make decisions", + "Risk First", + "Trailing Take-Profit", + "Trend Following", + "Scaling", "JSON", "symbol", "action", @@ -54,7 +54,7 @@ func TestPromptBuilder(t *testing.T) { } } - // 验证包含所有有效的action类型 + // Verify it contains all valid action types actions := []string{"HOLD", "PARTIAL_CLOSE", "FULL_CLOSE", "ADD_POSITION", "OPEN_NEW", "WAIT"} for _, action := range actions { if !strings.Contains(systemPrompt, action) { @@ -71,7 +71,7 @@ func TestPromptBuilder(t *testing.T) { t.Fatal("System prompt is empty") } - // 验证包含关键内容 + // Verify it contains key content mustContain := []string{ "quantitative trading AI", "Analyze Account Status", @@ -96,7 +96,7 @@ func TestPromptBuilder(t *testing.T) { }) t.Run("BuildUserPrompt", func(t *testing.T) { - // 创建测试上下文 + // Create test context ctx := createTestContext() builderZH := NewPromptBuilder(LangChinese) @@ -106,27 +106,27 @@ func TestPromptBuilder(t *testing.T) { t.Fatal("User prompt is empty") } - // 验证包含数据字典 - if !strings.Contains(userPromptZH, "数据字典") { + // Verify it contains the data dictionary + if !strings.Contains(userPromptZH, "Data Dictionary") { t.Error("User prompt should contain data dictionary") } - // 验证包含账户信息 + // Verify it contains account information if !strings.Contains(userPromptZH, "3079.40") { // Equity t.Error("User prompt should contain account equity") } - // 验证包含持仓信息 + // Verify it contains position information if !strings.Contains(userPromptZH, "PIPPINUSDT") { t.Error("User prompt should contain position symbol") } - // 验证包含决策要求 - if !strings.Contains(userPromptZH, "现在请做出决策") { + // Verify it contains decision requirements + if !strings.Contains(userPromptZH, "Now Make Your Decision") { t.Error("User prompt should contain decision requirements") } - // 英文版本 + // English version builderEN := NewPromptBuilder(LangEnglish) userPromptEN := builderEN.BuildUserPrompt(ctx) @@ -140,7 +140,7 @@ func TestPromptBuilder(t *testing.T) { }) } -// TestValidateDecisionFormat 测试决策格式验证 +// TestValidateDecisionFormat tests decision format validation func TestValidateDecisionFormat(t *testing.T) { t.Run("ValidDecision", func(t *testing.T) { decisions := []Decision{ @@ -152,7 +152,7 @@ func TestValidateDecisionFormat(t *testing.T) { StopLoss: 42000, TakeProfit: 48000, Confidence: 85, - Reasoning: "详细的推理过程", + Reasoning: "Detailed reasoning", }, } @@ -319,7 +319,7 @@ func TestValidateDecisionFormat(t *testing.T) { }, } - // OPEN_NEW需要额外字段 + // OPEN_NEW requires extra fields if action == "OPEN_NEW" { decisions[0].Leverage = 3 decisions[0].PositionSizeUSD = 1000 @@ -333,7 +333,7 @@ func TestValidateDecisionFormat(t *testing.T) { }) } -// TestFormatDecisionExample 测试决策示例格式化 +// TestFormatDecisionExample tests decision example formatting func TestFormatDecisionExample(t *testing.T) { t.Run("Chinese", func(t *testing.T) { example := FormatDecisionExample(LangChinese) @@ -342,7 +342,7 @@ func TestFormatDecisionExample(t *testing.T) { t.Fatal("Decision example is empty") } - // 应该是有效的JSON + // Should be valid JSON if !strings.HasPrefix(strings.TrimSpace(example), "[") { t.Error("Example should be a JSON array") } @@ -359,14 +359,14 @@ func TestFormatDecisionExample(t *testing.T) { t.Fatal("Decision example is empty") } - // 验证是有效的JSON格式 + // Verify it is valid JSON format if !strings.HasPrefix(strings.TrimSpace(example), "[") { t.Error("Example should be a JSON array") } }) } -// BenchmarkBuildSystemPrompt 性能测试 +// BenchmarkBuildSystemPrompt performance benchmark func BenchmarkBuildSystemPrompt(b *testing.B) { builder := NewPromptBuilder(LangChinese) @@ -384,7 +384,7 @@ func BenchmarkBuildSystemPrompt(b *testing.B) { }) } -// BenchmarkBuildUserPrompt 性能测试 +// BenchmarkBuildUserPrompt performance benchmark func BenchmarkBuildUserPrompt(b *testing.B) { builder := NewPromptBuilder(LangChinese) ctx := createTestContext() @@ -403,7 +403,7 @@ func BenchmarkBuildUserPrompt(b *testing.B) { }) } -// createTestContext 创建测试用的交易上下文 +// createTestContext creates a trading context for tests func createTestContext() *Context { return &Context{ CurrentTime: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"), diff --git a/kernel/schema.go b/kernel/schema.go index 91119da1..48c84ac3 100644 --- a/kernel/schema.go +++ b/kernel/schema.go @@ -62,156 +62,156 @@ func (d BilingualFieldDef) GetDesc(lang Language) string { var DataDictionary = map[string]map[string]BilingualFieldDef{ "AccountMetrics": { "Equity": { - NameZH: "总权益", + NameZH: "Total Equity", NameEN: "Total Equity", Unit: "USDT", - FormulaZH: "可用余额 + 未实现盈亏", + FormulaZH: "Available Balance + Unrealized PnL", FormulaEN: "Available Balance + Unrealized PnL", - DescZH: "账户的实际净值,包含所有持仓的浮动盈亏", + DescZH: "Actual net value of the account, including unrealized P&L of all positions", DescEN: "Actual account value including all unrealized P&L from positions", }, "Balance": { - NameZH: "可用余额", + NameZH: "Available Balance", NameEN: "Available Balance", Unit: "USDT", - FormulaZH: "初始资金 + 已实现盈亏", + FormulaZH: "Initial Capital + Realized PnL", FormulaEN: "Initial Capital + Realized PnL", - DescZH: "可用于开新仓位的资金,不包括已用保证金", + DescZH: "Funds available for opening new positions, excluding used margin", DescEN: "Available funds for opening new positions, excluding used margin", }, "PnL": { - NameZH: "总盈亏百分比", + NameZH: "Total PnL Percentage", NameEN: "Total PnL Percentage", Unit: "%", - FormulaZH: "(总权益 - 初始资金) / 初始资金 × 100", + FormulaZH: "(Total Equity - Initial Capital) / Initial Capital × 100", FormulaEN: "(Total Equity - Initial Capital) / Initial Capital × 100", - DescZH: "自系统启动以来的总收益率,+15.87%表示盈利15.87%", + DescZH: "Total return since system startup, +15.87% means 15.87% profit", DescEN: "Total return since inception, +15.87% means 15.87% profit", }, "Margin": { - NameZH: "保证金使用率", + NameZH: "Margin Usage Rate", NameEN: "Margin Usage Rate", Unit: "%", - FormulaZH: "已用保证金合计 / 总权益 × 100", + FormulaZH: "Total Used Margin / Total Equity × 100", FormulaEN: "Total Used Margin / Total Equity × 100", - DescZH: "该值越高,账户风险越大。安全值<30%,危险值>70%", + DescZH: "The higher this value, the greater the account risk. Safe <30%, Dangerous >70%", DescEN: "Higher value = higher risk. Safe <30%, Dangerous >70%", }, }, "TradeMetrics": { "Entry": { - NameZH: "进场价", + NameZH: "Entry Price", NameEN: "Entry Price", Unit: "USDT", - DescZH: "开仓时的平均价格", + DescZH: "Average price when opening the position", DescEN: "Average price when opening position", }, "Exit": { - NameZH: "出场价", + NameZH: "Exit Price", NameEN: "Exit Price", Unit: "USDT", - DescZH: "平仓时的平均价格", + DescZH: "Average price when closing the position", DescEN: "Average price when closing position", }, "Profit": { - NameZH: "已实现盈亏", + NameZH: "Realized PnL", NameEN: "Realized PnL", Unit: "USDT", - FormulaZH: "(出场价 - 进场价) / 进场价 × 杠杆 × 仓位价值", + FormulaZH: "(Exit Price - Entry Price) / Entry Price × Leverage × Position Value", FormulaEN: "(Exit Price - Entry Price) / Entry Price × Leverage × Position Value", - DescZH: "已平仓交易的实际盈亏,包含手续费。正值=盈利,负值=亏损", + DescZH: "Actual P&L of closed trades, including fees. Positive=profit, Negative=loss", DescEN: "Actual profit/loss of closed trades including fees. Positive=profit, Negative=loss", }, "PnL%": { - NameZH: "盈亏百分比", + NameZH: "PnL Percentage", NameEN: "PnL Percentage", Unit: "%", - FormulaZH: "(出场价 - 进场价) / 进场价 × 杠杆 × 100", + FormulaZH: "(Exit - Entry) / Entry × Leverage × 100", FormulaEN: "(Exit - Entry) / Entry × Leverage × 100", - DescZH: "已平仓交易的收益率,+6.71%表示盈利6.71%", + DescZH: "Return on a closed trade, +6.71% means 6.71% profit", DescEN: "Return on closed trade, +6.71% means 6.71% profit", }, "HoldDuration": { - NameZH: "持仓时长", + NameZH: "Holding Duration", NameEN: "Holding Duration", Unit: "minutes", - DescZH: "从开仓到平仓的时间。<15分钟=超短线,15分钟-4小时=日内,>4小时=波段", + DescZH: "Time from open to close. <15min=scalping, 15min-4h=intraday, >4h=swing", DescEN: "Time from open to close. <15min=scalping, 15min-4h=intraday, >4h=swing", }, }, "PositionMetrics": { "UnrealizedPnL%": { - NameZH: "未实现盈亏百分比", + NameZH: "Unrealized PnL Percentage", NameEN: "Unrealized PnL Percentage", Unit: "%", - FormulaZH: "(当前价 - 进场价) / 进场价 × 杠杆 × 100", + FormulaZH: "(Current Price - Entry Price) / Entry Price × Leverage × 100", FormulaEN: "(Current Price - Entry Price) / Entry Price × Leverage × 100", - DescZH: "当前持仓的浮动盈亏,未平仓前是浮动的", + DescZH: "Floating P&L of the current position, fluctuating until closed", DescEN: "Floating P&L of current position, not realized until closed", }, "PeakPnL%": { - NameZH: "峰值盈亏百分比", + NameZH: "Peak PnL Percentage", NameEN: "Peak PnL Percentage", Unit: "%", - DescZH: "该持仓曾经达到的最高未实现盈亏。用于判断是否需要止盈", + DescZH: "Highest unrealized P&L this position has reached. Used to decide whether to take profit", DescEN: "Historical max unrealized PnL for this position. Used for take-profit decisions", }, "Drawdown": { - NameZH: "从峰值回撤", + NameZH: "Drawdown from Peak", NameEN: "Drawdown from Peak", Unit: "%", - FormulaZH: "当前盈亏% - 峰值盈亏%", + FormulaZH: "Current PnL% - Peak PnL%", FormulaEN: "Current PnL% - Peak PnL%", - DescZH: "负值表示正在回撤。例如:峰值+5%,当前+3%,回撤=-2%", + DescZH: "Negative value means pulling back. E.g., Peak +5%, Current +3%, Drawdown = -2%", DescEN: "Negative = pulling back. E.g., Peak +5%, Current +3%, Drawdown = -2%", }, "Leverage": { - NameZH: "杠杆倍数", + NameZH: "Leverage", NameEN: "Leverage", Unit: "x", - DescZH: "3x表示价格变动1%,持仓盈亏变动3%。杠杆越高,风险越大", + DescZH: "3x means a 1% price move = 3% position PnL. Higher leverage = higher risk", DescEN: "3x means 1% price move = 3% position PnL. Higher leverage = higher risk", }, "Margin": { - NameZH: "占用保证金", + NameZH: "Margin Used", NameEN: "Margin Used", Unit: "USDT", - FormulaZH: "仓位价值 / 杠杆", + FormulaZH: "Position Value / Leverage", FormulaEN: "Position Value / Leverage", - DescZH: "该仓位锁定的保证金金额", + DescZH: "Amount of margin locked for this position", DescEN: "Collateral locked for this position", }, "LiqPrice": { - NameZH: "强平价格", + NameZH: "Liquidation Price", NameEN: "Liquidation Price", Unit: "USDT", - DescZH: "价格触及此值时会被强制平仓。0.0000表示无爆仓风险", + DescZH: "Position will be force-closed when price reaches this value. 0.0000 = no liquidation risk", DescEN: "Price at which position will be force-closed. 0.0000 = no liquidation risk", }, }, "MarketData": { "Volume": { - NameZH: "成交量", + NameZH: "Volume", NameEN: "Volume", Unit: "base asset", - DescZH: "该时间段的交易量", + DescZH: "Trading volume in this period", DescEN: "Trading volume in this period", }, "OI": { - NameZH: "持仓量", + NameZH: "Open Interest", NameEN: "Open Interest", Unit: "USDT", - DescZH: "未平仓合约的总价值。持仓量增加=资金流入,减少=资金流出", + DescZH: "Total value of open contracts. Increasing OI = capital inflow, decreasing = outflow", DescEN: "Total value of open contracts. Increasing OI = capital inflow, decreasing = outflow", }, "OIChange": { - NameZH: "持仓量变化", + NameZH: "OI Change", NameEN: "OI Change", Unit: "USDT & %", - DescZH: "1小时内持仓量的变化。用于判断市场真实资金流向", + DescZH: "OI change within 1 hour. Used to judge the real market capital flow direction", DescEN: "OI change in 1 hour. Used to determine real capital flow direction", }, }, @@ -256,30 +256,30 @@ var TradingRules = struct { RiskManagement: map[string]BilingualRuleDef{ "MaxMarginUsage": { Value: 0.30, - DescZH: "保证金使用率不得超过30%", + DescZH: "Margin usage must not exceed 30%", DescEN: "Margin usage must not exceed 30%", - ReasonZH: "保留70%的资金应对极端行情和追加保证金", + ReasonZH: "Reserve 70% capital for extreme market conditions and margin calls", ReasonEN: "Reserve 70% capital for extreme market conditions and margin calls", }, "MaxPositionLoss": { Value: -0.05, - DescZH: "单个持仓亏损达到-5%时必须止损", + DescZH: "Must stop-loss when single position loss reaches -5%", DescEN: "Must stop-loss when single position loss reaches -5%", - ReasonZH: "避免单笔交易造成过大损失", + ReasonZH: "Prevent excessive loss from single trade", ReasonEN: "Prevent excessive loss from single trade", }, "MaxDailyLoss": { Value: -0.10, - DescZH: "单日亏损达到-10%时停止交易", + DescZH: "Stop trading when daily loss reaches -10%", DescEN: "Stop trading when daily loss reaches -10%", - ReasonZH: "防止情绪化交易导致连续亏损", + ReasonZH: "Prevent emotional trading leading to consecutive losses", ReasonEN: "Prevent emotional trading leading to consecutive losses", }, "PositionSizeLimit": { Value: 0.15, - DescZH: "单个仓位不得超过总权益的15%", + DescZH: "Single position must not exceed 15% of total equity", DescEN: "Single position must not exceed 15% of total equity", - ReasonZH: "避免过度集中风险", + ReasonZH: "Avoid excessive risk concentration", ReasonEN: "Avoid excessive risk concentration", }, }, @@ -287,16 +287,16 @@ var TradingRules = struct { EntrySignals: map[string]BilingualRuleDef{ "VolumeSpike": { Value: 2.0, - DescZH: "成交量是平均值的2倍以上时考虑进场", + DescZH: "Consider entry when volume is 2x above average", DescEN: "Consider entry when volume is 2x above average", - ReasonZH: "放量突破通常意味着强趋势", + ReasonZH: "Volume breakout usually indicates strong trend", ReasonEN: "Volume breakout usually indicates strong trend", }, "OIChangeThreshold": { Value: 0.02, - DescZH: "持仓量1小时内变化超过2%视为显著变化", + DescZH: "OI change >2% in 1 hour is considered significant", DescEN: "OI change >2% in 1 hour is considered significant", - ReasonZH: "大额资金进出会导致持仓量显著变化", + ReasonZH: "Large capital flows cause significant OI changes", ReasonEN: "Large capital flows cause significant OI changes", }, }, @@ -304,16 +304,16 @@ var TradingRules = struct { ExitSignals: map[string]BilingualRuleDef{ "TrailingStop": { Value: 0.30, - DescZH: "当盈亏从峰值回撤30%时平仓止盈", + DescZH: "Close position when PnL pulls back 30% from peak", DescEN: "Close position when PnL pulls back 30% from peak", - ReasonZH: "锁定大部分利润,避免盈利回吐。例如:峰值+5%,回撤到+3.5%时平仓", + ReasonZH: "Lock in most profits, avoid profit giveback. E.g., Peak +5%, close at +3.5%", ReasonEN: "Lock in most profits, avoid profit giveback. E.g., Peak +5%, close at +3.5%", }, "StopLoss": { Value: -0.05, - DescZH: "硬止损设置在-5%", + DescZH: "Hard stop-loss at -5%", DescEN: "Hard stop-loss at -5%", - ReasonZH: "严格控制单笔最大损失", + ReasonZH: "Strictly control maximum single-trade loss", ReasonEN: "Strictly control maximum single-trade loss", }, }, @@ -321,9 +321,9 @@ var TradingRules = struct { PositionControl: map[string]BilingualRuleDef{ "ScaleIn": { Value: map[string]interface{}{"enabled": true, "max_additions": 2, "price_requirement": 0.01}, - DescZH: "只在盈利仓位上加仓,最多加2次,价格需比平均成本高1%", + DescZH: "Only add to winning positions, max 2 additions, price must be 1% above avg cost", DescEN: "Only add to winning positions, max 2 additions, price must be 1% above avg cost", - ReasonZH: "顺势加仓,不追亏损", + ReasonZH: "Add to winners, never average down losers", ReasonEN: "Add to winners, never average down losers", }, "ScaleOut": { @@ -332,9 +332,9 @@ var TradingRules = struct { {"pnl": 0.05, "close_pct": 0.50}, {"pnl": 0.08, "close_pct": 1.00}, }, - DescZH: "分批止盈:盈利3%时平33%,5%时平50%,8%时全平", + DescZH: "Scale-out: Close 33% at +3%, 50% at +5%, 100% at +8%", DescEN: "Scale-out: Close 33% at +3%, 50% at +5%, 100% at +8%", - ReasonZH: "在保证利润的同时让盈利奔跑", + ReasonZH: "Lock profits while letting winners run", ReasonEN: "Lock profits while letting winners run", }, }, @@ -367,28 +367,28 @@ var OIInterpretation = OIInterpretationType{ ZH string EN string }{ - ZH: "强多头趋势(新多单开仓,资金流入做多)", + ZH: "Strong bullish trend (new longs opening, capital flowing into long positions)", EN: "Strong bullish trend (new longs opening, capital flowing into long positions)", }, OIUp_PriceDown: struct { ZH string EN string }{ - ZH: "强空头趋势(新空单开仓,资金流入做空)", + ZH: "Strong bearish trend (new shorts opening, capital flowing into short positions)", EN: "Strong bearish trend (new shorts opening, capital flowing into short positions)", }, OIDown_PriceUp: struct { ZH string EN string }{ - ZH: "空头平仓(空头止损离场,可能出现反转)", + ZH: "Shorts covering (shorts stopped out, potential reversal)", EN: "Shorts covering (shorts stopped out, potential reversal)", }, OIDown_PriceDown: struct { ZH string EN string }{ - ZH: "多头平仓(多头止损离场,可能出现反转)", + ZH: "Longs closing (longs stopped out, potential reversal)", EN: "Longs closing (longs stopped out, potential reversal)", }, } @@ -407,35 +407,35 @@ type CommonMistake struct { var CommonMistakes = []CommonMistake{ { - ErrorZH: "混淆已实现盈亏和未实现盈亏", + ErrorZH: "Confusing realized and unrealized P&L", ErrorEN: "Confusing realized and unrealized P&L", - ExampleZH: "将历史交易的盈亏与当前持仓的盈亏相加", + ExampleZH: "Adding historical trade P&L with current position P&L", ExampleEN: "Adding historical trade P&L with current position P&L", - CorrectZH: "已实现盈亏已经计入账户余额,不应重复计算", + CorrectZH: "Realized P&L is already included in account balance, don't double count", CorrectEN: "Realized P&L is already included in account balance, don't double count", }, { - ErrorZH: "忽略杠杆对盈亏的影响", + ErrorZH: "Ignoring leverage's impact on P&L", ErrorEN: "Ignoring leverage's impact on P&L", - ExampleZH: "价格涨1%,认为盈利1%", + ExampleZH: "Price up 1%, thinking profit is 1%", ExampleEN: "Price up 1%, thinking profit is 1%", - CorrectZH: "3x杠杆时,价格涨1%,实际盈利约3%", + CorrectZH: "With 3x leverage, 1% price move = ~3% P&L", CorrectEN: "With 3x leverage, 1% price move = ~3% P&L", }, { - ErrorZH: "不理解Peak PnL的重要性", + ErrorZH: "Not understanding Peak PnL's importance", ErrorEN: "Not understanding Peak PnL's importance", - ExampleZH: "只关注当前PnL,不关注回撤", + ExampleZH: "Only watching current PnL, ignoring drawdown", ExampleEN: "Only watching current PnL, ignoring drawdown", - CorrectZH: "当前PnL接近Peak PnL时,应考虑止盈以锁定利润", + CorrectZH: "When current PnL near Peak PnL, consider taking profit to lock in gains", CorrectEN: "When current PnL near Peak PnL, consider taking profit to lock in gains", }, { - ErrorZH: "忽略持仓量(OI)变化", + ErrorZH: "Ignoring Open Interest changes", ErrorEN: "Ignoring Open Interest changes", - ExampleZH: "只看价格K线,不看资金流向", + ExampleZH: "Only watching price candles, not capital flows", ExampleEN: "Only watching price candles, not capital flows", - CorrectZH: "结合OI变化判断趋势的真实性和持续性", + CorrectZH: "Use OI changes to validate trend authenticity and sustainability", CorrectEN: "Use OI changes to validate trend authenticity and sustainability", }, } @@ -452,39 +452,39 @@ func GetSchemaPrompt(lang Language) string { // getSchemaPromptZH generates the Chinese prompt func getSchemaPromptZH() string { - prompt := "# 📖 数据字典与交易规则\n\n" - prompt += "## 📊 字段含义说明\n\n" + prompt := "# 📖 Data Dictionary & Trading Rules\n\n" + prompt += "## 📊 Field Definitions\n\n" // Account metrics - prompt += "### 账户指标\n" + prompt += "### Account Metrics\n" for key, field := range DataDictionary["AccountMetrics"] { prompt += formatFieldDefZH(key, field) } // Trade metrics - prompt += "\n### 交易指标\n" + prompt += "\n### Trade Metrics\n" for key, field := range DataDictionary["TradeMetrics"] { prompt += formatFieldDefZH(key, field) } // Position metrics - prompt += "\n### 持仓指标\n" + prompt += "\n### Position Metrics\n" for key, field := range DataDictionary["PositionMetrics"] { prompt += formatFieldDefZH(key, field) } // Market data - prompt += "\n### 市场数据\n" + prompt += "\n### Market Data\n" for key, field := range DataDictionary["MarketData"] { prompt += formatFieldDefZH(key, field) } // OI interpretation - prompt += "\n## 💹 持仓量(OI)变化解读\n\n" - prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n" - prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n" - prompt += "- **OI减少 + 价格上涨**: " + OIInterpretation.OIDown_PriceUp.ZH + "\n" - prompt += "- **OI减少 + 价格下跌**: " + OIInterpretation.OIDown_PriceDown.ZH + "\n" + prompt += "\n## 💹 Open Interest (OI) Change Interpretation\n\n" + prompt += "- **OI Up + Price Up**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n" + prompt += "- **OI Up + Price Down**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n" + prompt += "- **OI Down + Price Up**: " + OIInterpretation.OIDown_PriceUp.ZH + "\n" + prompt += "- **OI Down + Price Down**: " + OIInterpretation.OIDown_PriceDown.ZH + "\n" return prompt } @@ -530,12 +530,12 @@ func getSchemaPromptEN() string { // formatFieldDefZH formats a field definition in Chinese func formatFieldDefZH(key string, field BilingualFieldDef) string { - result := "- **" + key + "**(" + field.NameZH + "): " + field.DescZH + result := "- **" + key + "** (" + field.NameZH + "): " + field.DescZH if field.FormulaZH != "" { - result += " | 公式: `" + field.FormulaZH + "`" + result += " | Formula: `" + field.FormulaZH + "`" } if field.Unit != "" { - result += " | 单位: " + field.Unit + result += " | Unit: " + field.Unit } result += "\n" return result diff --git a/manager/trader_manager.go b/manager/trader_manager.go index a566b810..67ada63d 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -658,7 +658,16 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg return fmt.Errorf("failed to parse strategy config for trader %s: %w", traderCfg.Name, err) } strategyConfig.ClampLimits() - logger.Infof("✓ Trader %s loaded strategy config: %s", traderCfg.Name, strategy.Name) + // Autopilot (vergex_signal/claw402) runs a balanced multi-position book: + // hold several instruments with a smaller per-position notional so multiple + // long/short positions fit the margin. Applied after ClampLimits so it is + // not capped back to the conservative single-position default. + if strategyConfig.CoinSource.SourceType == "vergex_signal" { + strategyConfig.RiskControl.MaxPositions = 6 + strategyConfig.RiskControl.BTCETHMaxPositionValueRatio = 1.2 + strategyConfig.RiskControl.AltcoinMaxPositionValueRatio = 1.2 + } + logger.Infof("✓ Trader %s loaded strategy config: %s (maxPos=%d, posRatio=%.1f)", traderCfg.Name, strategy.Name, strategyConfig.RiskControl.MaxPositions, strategyConfig.RiskControl.AltcoinMaxPositionValueRatio) ensureHyperliquidNativeStrategy(traderCfg.Name, exchangeCfg.ExchangeType, strategyConfig) } else { return fmt.Errorf("trader %s has no strategy configured", traderCfg.Name) diff --git a/mcp/payment/claw402.go b/mcp/payment/claw402.go index 38edbb6b..fe7d50bc 100644 --- a/mcp/payment/claw402.go +++ b/mcp/payment/claw402.go @@ -78,7 +78,7 @@ var claw402ModelEndpoints = map[string]string{ "gemini-3.1-pro": "/api/v1/ai/gemini/chat/3.1-pro", // Kimi "kimi-k2.5": "/api/v1/ai/kimi/chat/k2.5", - // Z.AI (智谱) + // Z.AI (Zhipu) "glm-5": "/api/v1/ai/zhipu/chat", "glm-5-turbo": "/api/v1/ai/zhipu/chat/turbo", } diff --git a/provider/coinank/coinank_api/kline_test.go b/provider/coinank/coinank_api/kline_test.go index fe0f7ec9..365e1c36 100644 --- a/provider/coinank/coinank_api/kline_test.go +++ b/provider/coinank/coinank_api/kline_test.go @@ -27,10 +27,10 @@ func TestKlineDaily(t *testing.T) { t.Fatal(err) } - t.Log("=== BTCUSDT 日线 K线数据 (coinank_api 免费接口) ===") + t.Log("=== BTCUSDT daily K-line data (coinank_api free endpoint) ===") for i, k := range resp { startTime := time.UnixMilli(k.StartTime).Format("2006-01-02 15:04:05") - t.Logf("\n[%d] 时间: %s", i, startTime) + t.Logf("\n[%d] Time: %s", i, startTime) t.Logf(" Open: %.2f", k.Open) t.Logf(" High: %.2f", k.High) t.Logf(" Low: %.2f", k.Low) @@ -39,15 +39,15 @@ func TestKlineDaily(t *testing.T) { t.Logf(" Quantity: %.4f (k[7])", k.Quantity) t.Logf(" Count: %.0f (k[8])", k.Count) - // 计算验证 + // Calculation verification if k.Close > 0 && k.Volume > 0 { - t.Logf(" --- 验证 ---") + t.Logf(" --- verification ---") t.Logf(" Volume × Close = %.2f", k.Volume*k.Close) t.Logf(" Quantity / Close = %.4f", k.Quantity/k.Close) } } - // 打印原始 JSON + // Print raw JSON res, _ := json.MarshalIndent(resp, "", " ") - fmt.Printf("\n原始 JSON:\n%s\n", res) + fmt.Printf("\nRaw JSON:\n%s\n", res) } diff --git a/provider/hyperliquid/kline_test.go b/provider/hyperliquid/kline_test.go index e3859b37..bccc84f7 100644 --- a/provider/hyperliquid/kline_test.go +++ b/provider/hyperliquid/kline_test.go @@ -16,10 +16,10 @@ func TestGetCandles_BTC(t *testing.T) { t.Fatal(err) } - t.Log("=== BTC 日线数据 (Hyperliquid) ===") + t.Log("=== BTC daily data (Hyperliquid) ===") for i, c := range candles { openTime := time.UnixMilli(c.OpenTime).Format("2006-01-02 15:04:05") - t.Logf("\n[%d] 时间: %s", i, openTime) + t.Logf("\n[%d] Time: %s", i, openTime) t.Logf(" Symbol: %s", c.Symbol) t.Logf(" Interval: %s", c.Interval) t.Logf(" Open: %s", c.Open) @@ -30,24 +30,24 @@ func TestGetCandles_BTC(t *testing.T) { t.Logf(" TradeCount: %d", c.TradeCount) } - // 打印原始 JSON + // Print raw JSON res, _ := json.MarshalIndent(candles, "", " ") - fmt.Printf("\n原始 JSON:\n%s\n", res) + fmt.Printf("\nRaw JSON:\n%s\n", res) } func TestGetCandles_TSLA(t *testing.T) { client := NewClient() - // 测试股票永续合约 - 使用 xyz dex + // Test stock perpetual contracts - using xyz dex candles, err := client.GetCandles(context.TODO(), "TSLA", "1d", 5) if err != nil { t.Fatal(err) } - t.Log("=== TSLA 日线数据 (Hyperliquid xyz dex) ===") + t.Log("=== TSLA daily data (Hyperliquid xyz dex) ===") for i, c := range candles { openTime := time.UnixMilli(c.OpenTime).Format("2006-01-02 15:04:05") - t.Logf("\n[%d] 时间: %s", i, openTime) + t.Logf("\n[%d] Time: %s", i, openTime) t.Logf(" Symbol: %s", c.Symbol) t.Logf(" Interval: %s", c.Interval) t.Logf(" Open: %s", c.Open) @@ -58,33 +58,33 @@ func TestGetCandles_TSLA(t *testing.T) { t.Logf(" TradeCount: %d", c.TradeCount) } - // 打印原始 JSON + // Print raw JSON res, _ := json.MarshalIndent(candles, "", " ") - fmt.Printf("\n原始 JSON:\n%s\n", res) + fmt.Printf("\nRaw JSON:\n%s\n", res) } func TestGetCandles_StockPerps(t *testing.T) { client := NewClient() - // 测试多个股票永续合约 (xyz dex) + // Test multiple stock perpetual contracts (xyz dex) symbols := []string{"TSLA", "NVDA", "AAPL", "MSFT"} for _, symbol := range symbols { - t.Logf("\n=== %s 日线数据 ===", symbol) + t.Logf("\n=== %s daily data ===", symbol) candles, err := client.GetCandles(context.TODO(), symbol, "1d", 3) if err != nil { - t.Errorf("%s 获取失败: %v", symbol, err) + t.Errorf("%s fetch failed: %v", symbol, err) continue } if len(candles) == 0 { - t.Logf("%s: 无数据", symbol) + t.Logf("%s: no data", symbol) continue } latest := candles[len(candles)-1] openTime := time.UnixMilli(latest.OpenTime).Format("2006-01-02") - t.Logf("%s 最新: %s Open=%s High=%s Low=%s Close=%s Vol=%s", + t.Logf("%s latest: %s Open=%s High=%s Low=%s Close=%s Vol=%s", symbol, openTime, latest.Open, latest.High, latest.Low, latest.Close, latest.Volume) } } @@ -97,19 +97,19 @@ func TestGetAllMids(t *testing.T) { t.Fatal(err) } - t.Log("=== 加密货币资产中间价 (默认 dex) ===") + t.Log("=== Crypto asset mid prices (default dex) ===") - // 显示一些主要加密货币资产 + // Show some major crypto assets cryptoAssets := []string{"BTC", "ETH", "SOL", "DOGE", "XRP"} for _, asset := range cryptoAssets { if mid, ok := mids[asset]; ok { t.Logf("%s: %s", asset, mid) } else { - t.Logf("%s: 不存在", asset) + t.Logf("%s: not found", asset) } } - t.Logf("\n总共 %d 个加密货币交易对", len(mids)) + t.Logf("\nTotal %d crypto trading pairs", len(mids)) } func TestGetAllMidsXYZ(t *testing.T) { @@ -120,14 +120,14 @@ func TestGetAllMidsXYZ(t *testing.T) { t.Fatal(err) } - t.Log("=== xyz dex 资产中间价 (股票、外汇、大宗商品) ===") + t.Log("=== xyz dex asset mid prices (stocks, forex, commodities) ===") - // 显示所有 xyz dex 资产 + // Show all xyz dex assets for symbol, mid := range mids { t.Logf("%s: %s", symbol, mid) } - t.Logf("\n总共 %d 个 xyz dex 交易对", len(mids)) + t.Logf("\nTotal %d xyz dex trading pairs", len(mids)) } func TestGetMeta(t *testing.T) { @@ -138,11 +138,11 @@ func TestGetMeta(t *testing.T) { t.Fatal(err) } - t.Log("=== 资产元数据 ===") - t.Logf("总共 %d 个资产", len(meta.Universe)) + t.Log("=== Asset metadata ===") + t.Logf("Total %d assets", len(meta.Universe)) - // 显示股票永续合约 - t.Log("\n股票永续合约:") + // Show stock perpetual contracts + t.Log("\nStock perpetual contracts:") for _, asset := range meta.Universe { if IsStockPerp(asset.Name) { t.Logf(" %s: szDecimals=%d, maxLeverage=%d", asset.Name, asset.SzDecimals, asset.MaxLeverage) diff --git a/provider/nofxos/coin.go b/provider/nofxos/coin.go index 5eafffa3..5d5727e0 100644 --- a/provider/nofxos/coin.go +++ b/provider/nofxos/coin.go @@ -12,7 +12,7 @@ type QuantData struct { Symbol string `json:"symbol"` Price float64 `json:"price"` Netflow *NetflowData `json:"netflow,omitempty"` - OI map[string]*OIData `json:"oi,omitempty"` // keyed by exchange: "binance", "bybit" + OI map[string]*OIData `json:"oi,omitempty"` // keyed by exchange: "binance", "bybit" PriceChange map[string]float64 `json:"price_change,omitempty"` // keyed by duration: "1h", "4h", etc. } @@ -118,11 +118,11 @@ func FormatQuantDataForAI(symbol string, data *QuantData, lang Language) string func formatQuantDataZH(symbol string, data *QuantData) string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("### %s 量化数据\n", symbol)) - sb.WriteString(fmt.Sprintf("价格: $%.4f\n\n", data.Price)) + sb.WriteString(fmt.Sprintf("### %s Quant Data\n", symbol)) + sb.WriteString(fmt.Sprintf("Price: $%.4f\n\n", data.Price)) if len(data.PriceChange) > 0 { - sb.WriteString("**价格变化**:\n") + sb.WriteString("**Price Change**:\n") durations := []string{"1h", "4h", "8h", "12h", "24h"} for _, d := range durations { if change, ok := data.PriceChange[d]; ok { @@ -135,14 +135,14 @@ func formatQuantDataZH(symbol string, data *QuantData) string { if len(data.OI) > 0 { for exchange, oiData := range data.OI { if oiData != nil { - sb.WriteString(fmt.Sprintf("**%s持仓**:\n", strings.ToUpper(exchange))) + sb.WriteString(fmt.Sprintf("**%s Open Interest**:\n", strings.ToUpper(exchange))) sb.WriteString(fmt.Sprintf("- OI: %.2f\n", oiData.CurrentOI)) if oiData.NetLong > 0 || oiData.NetShort > 0 { - sb.WriteString(fmt.Sprintf("- 多头: %.2f, 空头: %.2f\n", oiData.NetLong, oiData.NetShort)) + sb.WriteString(fmt.Sprintf("- Long: %.2f, Short: %.2f\n", oiData.NetLong, oiData.NetShort)) } if oiData.Delta != nil { if delta, ok := oiData.Delta["1h"]; ok && delta != nil { - sb.WriteString(fmt.Sprintf("- 1h变化: %s (%.2f%%)\n", + sb.WriteString(fmt.Sprintf("- 1h Change: %s (%.2f%%)\n", formatValue(delta.OIDeltaValue), delta.OIDeltaPercent)) } } @@ -152,7 +152,7 @@ func formatQuantDataZH(symbol string, data *QuantData) string { } if data.Netflow != nil && data.Netflow.Institution != nil && data.Netflow.Institution.Future != nil { - sb.WriteString("**机构资金流**:\n") + sb.WriteString("**Institution Net Flow**:\n") durations := []string{"1h", "4h", "24h"} for _, d := range durations { if flow, ok := data.Netflow.Institution.Future[d]; ok { diff --git a/provider/nofxos/netflow.go b/provider/nofxos/netflow.go index 1d9958e6..735b145d 100644 --- a/provider/nofxos/netflow.go +++ b/provider/nofxos/netflow.go @@ -22,8 +22,8 @@ type NetFlowResponse struct { Data struct { Netflows []NetFlowPosition `json:"netflows"` Count int `json:"count"` - Type string `json:"type"` // institution or personal - Trade string `json:"trade"` // futures or spot + Type string `json:"type"` // institution or personal + Trade string `json:"trade"` // futures or spot TimeRange string `json:"time_range"` RankType string `json:"rank_type"` // top or low Limit int `json:"limit"` @@ -131,13 +131,13 @@ func FormatNetFlowRankingForAI(data *NetFlowRankingData, lang Language) string { func formatNetFlowRankingZH(data *NetFlowRankingData) string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("## 资金流向排行 (%s)\n\n", data.Duration)) + sb.WriteString(fmt.Sprintf("## Net Flow Ranking (%s)\n\n", data.Duration)) // Institution inflow if len(data.InstitutionFutureTop) > 0 { - sb.WriteString("### 机构资金流入榜\n") - sb.WriteString("Smart Money买入信号:\n\n") - sb.WriteString("| 排名 | 币种 | 流入金额(USDT) | 价格 |\n") + sb.WriteString("### Institution Inflow Ranking\n") + sb.WriteString("Smart Money buy signal:\n\n") + sb.WriteString("| Rank | Symbol | Inflow Amount(USDT) | Price |\n") sb.WriteString("|------|------|----------------|------|\n") for _, pos := range data.InstitutionFutureTop { sb.WriteString(fmt.Sprintf("| %d | %s | %s | $%.4f |\n", @@ -148,9 +148,9 @@ func formatNetFlowRankingZH(data *NetFlowRankingData) string { // Institution outflow if len(data.InstitutionFutureLow) > 0 { - sb.WriteString("### 机构资金流出榜\n") - sb.WriteString("Smart Money卖出信号:\n\n") - sb.WriteString("| 排名 | 币种 | 流出金额(USDT) | 价格 |\n") + sb.WriteString("### Institution Outflow Ranking\n") + sb.WriteString("Smart Money sell signal:\n\n") + sb.WriteString("| Rank | Symbol | Outflow Amount(USDT) | Price |\n") sb.WriteString("|------|------|----------------|------|\n") for _, pos := range data.InstitutionFutureLow { sb.WriteString(fmt.Sprintf("| %d | %s | %s | $%.4f |\n", @@ -161,9 +161,9 @@ func formatNetFlowRankingZH(data *NetFlowRankingData) string { // Retail flow summary if len(data.PersonalFutureTop) > 0 || len(data.PersonalFutureLow) > 0 { - sb.WriteString("### 散户资金动向\n") + sb.WriteString("### Retail Capital Movement\n") if len(data.PersonalFutureTop) > 0 { - sb.WriteString("散户买入: ") + sb.WriteString("Retail buy: ") for i, pos := range data.PersonalFutureTop { if i >= 3 { break @@ -176,7 +176,7 @@ func formatNetFlowRankingZH(data *NetFlowRankingData) string { sb.WriteString("\n") } if len(data.PersonalFutureLow) > 0 { - sb.WriteString("散户卖出: ") + sb.WriteString("Retail sell: ") for i, pos := range data.PersonalFutureLow { if i >= 3 { break @@ -191,7 +191,7 @@ func formatNetFlowRankingZH(data *NetFlowRankingData) string { sb.WriteString("\n") } - sb.WriteString("**解读**: 机构买入+散户卖出=强烈看多 | 机构卖出+散户买入=强烈看空\n\n") + sb.WriteString("**Interpretation**: Institution buy + Retail sell = strongly bullish | Institution sell + Retail buy = strongly bearish\n\n") return sb.String() } diff --git a/provider/nofxos/oi.go b/provider/nofxos/oi.go index 2819379e..a0272855 100644 --- a/provider/nofxos/oi.go +++ b/provider/nofxos/oi.go @@ -169,12 +169,12 @@ func FormatOIRankingForAI(data *OIRankingData, lang Language) string { func formatOIRankingZH(data *OIRankingData) string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("## 持仓量变化排行 (%s)\n\n", data.Duration)) + sb.WriteString(fmt.Sprintf("## Open Interest Change Ranking (%s)\n\n", data.Duration)) if len(data.TopPositions) > 0 { - sb.WriteString("### 持仓增加榜\n") - sb.WriteString("资金流入,趋势延续或新仓建立信号:\n\n") - sb.WriteString("| 排名 | 币种 | 持仓变化(USDT) | OI变化% | 价格变化% |\n") + sb.WriteString("### OI Increase Ranking\n") + sb.WriteString("Capital inflow, trend continuation or new position signal:\n\n") + sb.WriteString("| Rank | Symbol | OI Change(USDT) | OI Change% | Price Change% |\n") sb.WriteString("|------|------|----------------|---------|----------|\n") for _, pos := range data.TopPositions { sb.WriteString(fmt.Sprintf("| %d | %s | %s | %+.2f%% | %+.2f%% |\n", @@ -185,9 +185,9 @@ func formatOIRankingZH(data *OIRankingData) string { } if len(data.LowPositions) > 0 { - sb.WriteString("### 持仓减少榜\n") - sb.WriteString("资金流出,趋势反转或仓位平仓信号:\n\n") - sb.WriteString("| 排名 | 币种 | 持仓变化(USDT) | OI变化% | 价格变化% |\n") + sb.WriteString("### OI Decrease Ranking\n") + sb.WriteString("Capital outflow, trend reversal or position close signal:\n\n") + sb.WriteString("| Rank | Symbol | OI Change(USDT) | OI Change% | Price Change% |\n") sb.WriteString("|------|------|----------------|---------|----------|\n") for _, pos := range data.LowPositions { sb.WriteString(fmt.Sprintf("| %d | %s | %s | %+.2f%% | %+.2f%% |\n", @@ -197,7 +197,7 @@ func formatOIRankingZH(data *OIRankingData) string { sb.WriteString("\n") } - sb.WriteString("**解读**: OI增+价涨=多头主导 | OI增+价跌=空头主导 | OI减+价涨=空头平仓 | OI减+价跌=多头平仓\n\n") + sb.WriteString("**Interpretation**: OI up + Price up = longs dominant | OI up + Price down = shorts dominant | OI down + Price up = shorts closing | OI down + Price down = longs closing\n\n") return sb.String() } diff --git a/provider/nofxos/price.go b/provider/nofxos/price.go index 4c39ab51..e3df067b 100644 --- a/provider/nofxos/price.go +++ b/provider/nofxos/price.go @@ -12,7 +12,7 @@ import ( type PriceRankingItem struct { Pair string `json:"pair"` Symbol string `json:"symbol"` - PriceDelta float64 `json:"price_delta"` // Decimal format: 0.0723 = 7.23% + PriceDelta float64 `json:"price_delta"` // Decimal format: 0.0723 = 7.23% Price float64 `json:"price"` FutureFlow float64 `json:"future_flow"` SpotFlow float64 `json:"spot_flow"` @@ -98,7 +98,7 @@ func FormatPriceRankingForAI(data *PriceRankingData, lang Language) string { func formatPriceRankingZH(data *PriceRankingData) string { var sb strings.Builder - sb.WriteString("## 涨跌幅排行\n\n") + sb.WriteString("## Price Change Ranking\n\n") durationOrder := []string{"1h", "4h", "24h"} for _, duration := range durationOrder { @@ -107,11 +107,11 @@ func formatPriceRankingZH(data *PriceRankingData) string { continue } - sb.WriteString(fmt.Sprintf("### %s 涨跌幅\n\n", duration)) + sb.WriteString(fmt.Sprintf("### %s Price Change\n\n", duration)) if len(durationData.Top) > 0 { - sb.WriteString("**涨幅榜**\n") - sb.WriteString("| 币种 | 涨幅 | 价格 | 资金流 | OI变化 |\n") + sb.WriteString("**Gainers**\n") + sb.WriteString("| Symbol | Gain | Price | Net Flow | OI Change |\n") sb.WriteString("|------|------|------|--------|--------|\n") for _, item := range durationData.Top { sb.WriteString(fmt.Sprintf("| %s | %+.2f%% | $%.4f | %s | %s |\n", @@ -122,8 +122,8 @@ func formatPriceRankingZH(data *PriceRankingData) string { } if len(durationData.Low) > 0 { - sb.WriteString("**跌幅榜**\n") - sb.WriteString("| 币种 | 跌幅 | 价格 | 资金流 | OI变化 |\n") + sb.WriteString("**Losers**\n") + sb.WriteString("| Symbol | Loss | Price | Net Flow | OI Change |\n") sb.WriteString("|------|------|------|--------|--------|\n") for _, item := range durationData.Low { sb.WriteString(fmt.Sprintf("| %s | %.2f%% | $%.4f | %s | %s |\n", @@ -134,7 +134,7 @@ func formatPriceRankingZH(data *PriceRankingData) string { } } - sb.WriteString("**解读**: 涨幅大+资金流入+OI增加=强势上涨 | 跌幅大+资金流出+OI减少=弱势下跌\n\n") + sb.WriteString("**Interpretation**: Large gain + capital inflow + OI increase = strong uptrend | Large loss + capital outflow + OI decrease = weak downtrend\n\n") return sb.String() } diff --git a/provider/vergex/client.go b/provider/vergex/client.go index 176d8418..389511cf 100644 --- a/provider/vergex/client.go +++ b/provider/vergex/client.go @@ -27,6 +27,7 @@ const ( SignalRankingPath = "/api/v1/vergex/signal-ranking" SignalLabPath = "/api/v1/vergex/signal-lab" CostLiquidationHeatmapPath = "/api/v1/vergex/cost-liquidation-heatmap" + FlowMarketsPath = "/api/v1/vergex/flow-markets" ) type Client struct { @@ -128,6 +129,24 @@ func (c *Client) GetCostLiquidationHeatmap(ctx context.Context, q Query) (json.R return c.doGET(ctx, CostLiquidationHeatmapPath, params) } +// GetFlowMarkets fetches the Vergex net-flow market ranking via the paid +// claw402 x402 endpoint. Params mirror the public API: chain (e.g. "mainnet"), +// window (e.g. "1h"), and limit. The raw JSON is returned for the caller to +// pass through — the response shape is owned by Vergex. +func (c *Client) GetFlowMarkets(ctx context.Context, chain, window string, limit int) (json.RawMessage, error) { + params := url.Values{} + if v := strings.TrimSpace(chain); v != "" { + params.Set("chain", v) + } + if v := strings.TrimSpace(window); v != "" { + params.Set("window", v) + } + if limit > 0 { + params.Set("limit", fmt.Sprintf("%d", limit)) + } + return c.doGET(ctx, FlowMarketsPath, params) +} + func addQueryDefaults(params url.Values, q Query, includeMarket bool) { if includeMarket { if q.MarketType != "" { diff --git a/store/strategy.go b/store/strategy.go index ec48bfc5..855ba863 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -13,7 +13,7 @@ import ( // Hard limits to prevent token explosion in AI requests const ( MaxCandidateCoins = 10 - MaxPositions = 3 + MaxPositions = 8 MaxTimeframes = 4 MinKlineCount = 10 MaxKlineCount = 30 @@ -268,9 +268,9 @@ func (c *StrategyConfig) NormalizeProductSchema() { func normalizeStrategyType(value string) string { value = strings.ToLower(strings.TrimSpace(value)) switch value { - case "grid", "grid_strategy", "grid-trading", "grid trading", "grid_trading", "网格", "网格策略", "网格交易": + case "grid", "grid_strategy", "grid-trading", "grid trading", "grid_trading", "grid strategy": return "grid_trading" - case "", "ai", "ai_strategy", "ai-trading", "ai trading", "ai_trading", "ai策略", "ai 策略", "ai交易策略", "ai智能策略": + case "", "ai", "ai_strategy", "ai-trading", "ai trading", "ai_trading", "ai strategy", "ai smart strategy": return "ai_trading" default: return value @@ -279,25 +279,25 @@ func normalizeStrategyType(value string) string { func normalizeCoinSourceType(value string) string { value = strings.ToLower(strings.TrimSpace(value)) - compact := strings.NewReplacer(" ", "", "_", "", "-", "", "数据源", "", "选币", "", "币种", "").Replace(value) + compact := strings.NewReplacer(" ", "", "_", "", "-", "", "datasource", "", "coinselection", "", "coin", "").Replace(value) switch { case compact == "": return "" case strings.Contains(compact, "ai500"): return "ai500" - case strings.Contains(compact, "oitop") || strings.Contains(value, "oi top") || strings.Contains(value, "持仓量最高") || strings.Contains(value, "持仓量靠前"): + case strings.Contains(compact, "oitop") || strings.Contains(value, "oi top") || strings.Contains(value, "highest open interest") || strings.Contains(value, "top open interest"): return "oi_top" - case strings.Contains(compact, "oilow") || strings.Contains(value, "oi low") || strings.Contains(value, "持仓量最低") || strings.Contains(value, "持仓量较低"): + case strings.Contains(compact, "oilow") || strings.Contains(value, "oi low") || strings.Contains(value, "lowest open interest") || strings.Contains(value, "low open interest"): return "oi_low" case strings.Contains(compact, "hyperrank"): return "hyper_rank" - case strings.Contains(compact, "vergex") || strings.Contains(compact, "claw402") || strings.Contains(compact, "dynamicranking") || strings.Contains(value, "动态榜单") || strings.Contains(value, "涨幅榜") || strings.Contains(value, "信号榜"): + case strings.Contains(compact, "vergex") || strings.Contains(compact, "claw402") || strings.Contains(compact, "dynamicranking") || strings.Contains(value, "dynamic board") || strings.Contains(value, "gainers board") || strings.Contains(value, "signal board"): return "vergex_signal" case strings.Contains(compact, "hyperall"): return "hyper_all" case strings.Contains(compact, "hypermain"): return "hyper_main" - case strings.Contains(value, "static") || strings.Contains(value, "固定") || strings.Contains(value, "静态"): + case strings.Contains(value, "static") || strings.Contains(value, "fixed"): return "static" default: return value @@ -395,25 +395,25 @@ func splitLooseStringList(values []string) []string { func normalizeTimeframe(value string) string { value = strings.ToLower(strings.TrimSpace(value)) - value = strings.Trim(value, "\"',,。 ") + value = strings.Trim(value, "\"', . ") if value == "" { return "" } aliases := map[string]string{ - "1分钟": "1m", - "3分钟": "3m", - "5分钟": "5m", - "15分钟": "15m", - "30分钟": "30m", - "1小时": "1h", - "2小时": "2h", - "4小时": "4h", - "6小时": "6h", - "8小时": "8h", - "12小时": "12h", - "1天": "1d", - "3天": "3d", - "1周": "1w", + "1 minute": "1m", + "3 minute": "3m", + "5 minute": "5m", + "15 minute": "15m", + "30 minute": "30m", + "1 hour": "1h", + "2 hour": "2h", + "4 hour": "4h", + "6 hour": "6h", + "8 hour": "8h", + "12 hour": "12h", + "1 day": "1d", + "3 day": "3d", + "1 week": "1w", } if alias, ok := aliases[value]; ok { return alias @@ -559,7 +559,7 @@ func StrategyClampWarnings(before, after StrategyConfig, lang string) []string { return } if lang == "zh" { - warnings = append(warnings, fmt.Sprintf("%s 已从 %d 调整为 %d", labelZH, from, to)) + warnings = append(warnings, fmt.Sprintf("%s adjusted from %d to %d", labelZH, from, to)) return } warnings = append(warnings, fmt.Sprintf("%s adjusted from %d to %d", labelEN, from, to)) @@ -569,21 +569,21 @@ func StrategyClampWarnings(before, after StrategyConfig, lang string) []string { return } if lang == "zh" { - warnings = append(warnings, fmt.Sprintf("%s 已从 %.2f 调整为 %.2f", labelZH, from, to)) + warnings = append(warnings, fmt.Sprintf("%s adjusted from %.2f to %.2f", labelZH, from, to)) return } warnings = append(warnings, fmt.Sprintf("%s adjusted from %.2f to %.2f", labelEN, from, to)) } - appendInt("最大持仓数", "max_positions", before.RiskControl.MaxPositions, after.RiskControl.MaxPositions) - appendInt("BTC/ETH 最大杠杆", "btc_eth_max_leverage", before.RiskControl.BTCETHMaxLeverage, after.RiskControl.BTCETHMaxLeverage) - appendInt("山寨币最大杠杆", "altcoin_max_leverage", before.RiskControl.AltcoinMaxLeverage, after.RiskControl.AltcoinMaxLeverage) - appendFloat("BTC/ETH 最大仓位价值倍数", "btc_eth_max_position_value_ratio", before.RiskControl.BTCETHMaxPositionValueRatio, after.RiskControl.BTCETHMaxPositionValueRatio) - appendFloat("山寨币最大仓位价值倍数", "altcoin_max_position_value_ratio", before.RiskControl.AltcoinMaxPositionValueRatio, after.RiskControl.AltcoinMaxPositionValueRatio) - appendFloat("最小盈亏比", "min_risk_reward_ratio", before.RiskControl.MinRiskRewardRatio, after.RiskControl.MinRiskRewardRatio) - appendFloat("最大保证金使用率", "max_margin_usage", before.RiskControl.MaxMarginUsage, after.RiskControl.MaxMarginUsage) - appendFloat("最小开仓金额", "min_position_size", before.RiskControl.MinPositionSize, after.RiskControl.MinPositionSize) - appendInt("最低置信度", "min_confidence", before.RiskControl.MinConfidence, after.RiskControl.MinConfidence) + appendInt("Max Positions", "max_positions", before.RiskControl.MaxPositions, after.RiskControl.MaxPositions) + appendInt("BTC/ETH Max Leverage", "btc_eth_max_leverage", before.RiskControl.BTCETHMaxLeverage, after.RiskControl.BTCETHMaxLeverage) + appendInt("Altcoin Max Leverage", "altcoin_max_leverage", before.RiskControl.AltcoinMaxLeverage, after.RiskControl.AltcoinMaxLeverage) + appendFloat("BTC/ETH Max Position Value Ratio", "btc_eth_max_position_value_ratio", before.RiskControl.BTCETHMaxPositionValueRatio, after.RiskControl.BTCETHMaxPositionValueRatio) + appendFloat("Altcoin Max Position Value Ratio", "altcoin_max_position_value_ratio", before.RiskControl.AltcoinMaxPositionValueRatio, after.RiskControl.AltcoinMaxPositionValueRatio) + appendFloat("Min Risk/Reward Ratio", "min_risk_reward_ratio", before.RiskControl.MinRiskRewardRatio, after.RiskControl.MinRiskRewardRatio) + appendFloat("Max Margin Usage", "max_margin_usage", before.RiskControl.MaxMarginUsage, after.RiskControl.MaxMarginUsage) + appendFloat("Min Position Size", "min_position_size", before.RiskControl.MinPositionSize, after.RiskControl.MinPositionSize) + appendInt("Min Confidence", "min_confidence", before.RiskControl.MinConfidence, after.RiskControl.MinConfidence) return warnings } @@ -1014,37 +1014,37 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig { PriceRankingLimit: 10, }, RiskControl: RiskControlConfig{ - MaxPositions: 2, // Max 2 instruments simultaneously (CODE ENFORCED) - BTCETHMaxLeverage: 10, // BTC/ETH exchange leverage (AI guided) - AltcoinMaxLeverage: 10, // TradeFi exchange leverage (AI guided) - BTCETHMaxPositionValueRatio: 10.0, // Claw402 full-size 10x notional: equity × 10 - AltcoinMaxPositionValueRatio: 10.0, // Claw402 full-size 10x notional: equity × 10 - MaxMarginUsage: 1.0, // Claw402 Autopilot intentionally uses full margin when opening - MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED) - MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided) - MinConfidence: 78, // Min 78% confidence (AI guided) + MaxPositions: 6, // Hold up to 6 instruments (≈3 long + 3 short) simultaneously (CODE ENFORCED) + BTCETHMaxLeverage: 10, // BTC/ETH exchange leverage (AI guided) + AltcoinMaxLeverage: 10, // TradeFi exchange leverage (AI guided) + BTCETHMaxPositionValueRatio: 1.2, // Per-position notional = equity × 1.2 so several positions fit the margin + AltcoinMaxPositionValueRatio: 1.2, // Per-position notional = equity × 1.2 so several positions fit the margin + MaxMarginUsage: 1.0, // Claw402 Autopilot intentionally uses full margin when opening + MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED) + MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided) + MinConfidence: 78, // Min 78% confidence (AI guided) }, } if lang == "zh" { config.PromptSections = PromptSectionsConfig{ - RoleDefinition: `# 你是 NOFX Claw402 自动交易员 + RoleDefinition: `# You are the NOFX Claw402 auto-trader -你只交易 Claw402.ai/Vergex 本轮榜单返回的 Hyperliquid 可交易标的。候选池来自 Claw402.ai/Vergex,开仓前必须结合 Signal Lab、成本/清算热力图和原始 K 线判断。`, - TradingFrequency: `# 交易频率 +Trade only the Hyperliquid tradable instruments returned by this cycle's Claw402.ai/Vergex board. The candidate pool comes from Claw402.ai/Vergex; before opening a position, you must combine Signal Lab, cost/liquidation heatmap and raw candles.`, + TradingFrequency: `# Trading Frequency -- 优先等待高质量机会,不需要每轮都交易。 -- 先管理已有持仓,再考虑新开仓。 -- 同一轮不要频繁开平同一标的。`, - EntryStandards: `# 入场标准 +- Prioritize waiting for high-quality opportunities; you do not need to trade every cycle. +- Manage existing positions first, then consider opening new ones. +- Do not churn in and out of the same symbol in one cycle.`, + EntryStandards: `# Entry Standards -只有 Claw402 Signal Lab、成本/清算热力图和原始 K 线大体一致时才开仓。Claw402 排名只是候选池,不是单独买入理由。任一关键数据缺失或冲突时,默认等待。`, - DecisionProcess: `# 决策流程 +Open a position only when Claw402 Signal Lab, cost/liquidation heatmap and raw candles broadly agree. The Claw402 ranking is only the candidate pool, not a standalone buy reason. Wait by default when any key data is missing or contradictory.`, + DecisionProcess: `# Decision Process -1. 检查已有持仓,先决定止盈、止损或继续持有。 -2. 从 Claw402 榜单取本轮候选,并对每个候选读取 Claw402 Ranking、Signal Lab、Cost/Liquidation Heatmap。 -3. 用原始 K 线确认入场位置、止损和止盈。 -4. 输出简洁 reasoning 和严格 JSON。`, +1. Check existing positions first: decide take profit, stop loss or hold. +2. Pull this cycle's candidates from the Claw402 board, and for each candidate read Claw402 Ranking, Signal Lab and Cost/Liquidation Heatmap. +3. Use raw candles to confirm entry, stop loss and take profit. +4. Output concise reasoning and strict JSON.`, } } else { config.PromptSections = PromptSectionsConfig{ diff --git a/store/strategy_schema_test.go b/store/strategy_schema_test.go index 1d3d52d2..89214860 100644 --- a/store/strategy_schema_test.go +++ b/store/strategy_schema_test.go @@ -80,14 +80,14 @@ func TestStrategyConfigUnmarshalLegacyFlatAIConfig(t *testing.T) { func TestStrategyConfigNormalizeProductSchemaForLLMLabels(t *testing.T) { cfg := GetDefaultStrategyConfig("zh") patch := map[string]any{ - "strategy_type": "AI 策略", + "strategy_type": "AI strategy", "ai_config": map[string]any{ "coin_source": map[string]any{ "source_type": "AI500", }, "indicators": map[string]any{ "klines": map[string]any{ - "primary_timeframe": "1分钟", + "primary_timeframe": "1 minute", "selected_timeframes": []any{`["1m"`, `"5m"`, `"15m"]`}, }, }, @@ -126,7 +126,7 @@ func TestStrategyConfigNormalizeProductSchemaForLLMLabels(t *testing.T) { func TestStrategyConfigNormalizeProductSchemaForVergexSignal(t *testing.T) { cfg := GetDefaultStrategyConfig("zh") cfg.CoinSource = CoinSourceConfig{ - SourceType: "Claw402 Vergex 信号榜", + SourceType: "Claw402 Vergex signal board", } cfg.NormalizeProductSchema() diff --git a/telegram/agent/agent.go b/telegram/agent/agent.go index 2f54ddff..6444ba00 100644 --- a/telegram/agent/agent.go +++ b/telegram/agent/agent.go @@ -273,7 +273,7 @@ func (a *Agent) Run(userMessage string, onChunk func(string)) string { // Safety: max iterations reached. logger.Warnf("Agent: max iterations (%d) reached for message: %q", maxIterations, userMessage) - reply := "Operation completed. Please check your account for the latest status. / 操作已完成,请检查您的账户查看最新状态。" + reply := "Operation completed. Please check your account for the latest status." a.memory.Add("user", userMessage) a.memory.Add("assistant", reply) return reply diff --git a/telegram/agent/agent_test.go b/telegram/agent/agent_test.go index ff2e0e41..7b252db5 100644 --- a/telegram/agent/agent_test.go +++ b/telegram/agent/agent_test.go @@ -213,7 +213,7 @@ func TestNarrationStructurallyImpossible(t *testing.T) { // Simulate a (malformed) response that has both Content and ToolCalls. malformed := &mcp.LLMResponse{ - Content: "现在我将为您查询策略。", // narration — must NOT reach user + Content: "Now I will look up your strategies.", // narration — must NOT reach user ToolCalls: []mcp.ToolCall{{ ID: "c1", Type: "function", @@ -226,15 +226,15 @@ func TestNarrationStructurallyImpossible(t *testing.T) { llm := &mockLLM{responses: []*mcp.LLMResponse{ malformed, - textReply("你有1个策略:BTC Trend。"), + textReply("You have 1 strategy: BTC Trend."), }} a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt) - reply := a.Run("查询我的策略", nil) + reply := a.Run("look up my strategies", nil) - if strings.Contains(reply, "现在我将") { + if strings.Contains(reply, "Now I will") { t.Fatalf("narration leaked into final reply: %q", reply) } - if reply != "你有1个策略:BTC Trend。" { + if reply != "You have 1 strategy: BTC Trend." { t.Fatalf("unexpected reply: %q", reply) } } @@ -276,18 +276,18 @@ func TestOnChunkCalledWithFinalReply(t *testing.T) { // Verifies: POST strategy → GET verify → final reply shows strategy info. func TestCreateStrategyWorkflow(t *testing.T) { srv, port := mockAPIServer(map[string]string{ - "POST /api/strategies": `{"id":"s1","name":"BTC趋势"}`, - "GET /api/strategies/s1": `{"id":"s1","name":"BTC趋势","config":{"coin_source":{"source_type":"static","static_coins":["BTC/USDT"]},"leverage":5}}`, + "POST /api/strategies": `{"id":"s1","name":"BTC Trend"}`, + "GET /api/strategies/s1": `{"id":"s1","name":"BTC Trend","config":{"coin_source":{"source_type":"static","static_coins":["BTC/USDT"]},"leverage":5}}`, }) defer srv.Close() llm := &mockLLM{responses: []*mcp.LLMResponse{ - toolCall("c1", "POST", "/api/strategies", `{"name":"BTC趋势","config":{}}`), + toolCall("c1", "POST", "/api/strategies", `{"name":"BTC Trend","config":{}}`), toolCall("c2", "GET", "/api/strategies/s1", "{}"), - textReply("策略已创建:BTC趋势,币种 BTC/USDT,杠杆 5x。"), + textReply("Strategy created: BTC Trend, coin BTC/USDT, leverage 5x."), }} a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt) - reply := a.Run("帮我配置个btc趋势交易的策略", nil) + reply := a.Run("set up a BTC trend trading strategy for me", nil) if llm.calls != 3 { t.Fatalf("expected 3 LLM calls, got %d", llm.calls) @@ -298,7 +298,7 @@ func TestCreateStrategyWorkflow(t *testing.T) { } // TestFullSetupWorkflow: create strategy → verify → create trader → start trader. -// This is the "帮我配置策略并跑起来" workflow. +// This is the "create strategy and start it" workflow. func TestFullSetupWorkflow(t *testing.T) { calls := map[string]int{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -306,11 +306,11 @@ func TestFullSetupWorkflow(t *testing.T) { calls[key]++ switch key { case "POST /api/strategies": - w.Write([]byte(`{"id":"s1","name":"BTC趋势"}`)) //nolint:errcheck + w.Write([]byte(`{"id":"s1","name":"BTC Trend"}`)) //nolint:errcheck case "GET /api/strategies/s1": - w.Write([]byte(`{"id":"s1","name":"BTC趋势","config":{}}`)) //nolint:errcheck + w.Write([]byte(`{"id":"s1","name":"BTC Trend","config":{}}`)) //nolint:errcheck case "POST /api/traders": - w.Write([]byte(`{"id":"tr1","name":"BTC趋势交易员"}`)) //nolint:errcheck + w.Write([]byte(`{"id":"tr1","name":"BTC Trend Trader"}`)) //nolint:errcheck case "POST /api/traders/tr1/start": w.Write([]byte(`{"ok":true}`)) //nolint:errcheck default: @@ -322,14 +322,14 @@ func TestFullSetupWorkflow(t *testing.T) { fmt.Sscanf(srv.Listener.Addr().String(), "127.0.0.1:%d", &port) llm := &mockLLM{responses: []*mcp.LLMResponse{ - toolCall("c1", "POST", "/api/strategies", `{"name":"BTC趋势"}`), + toolCall("c1", "POST", "/api/strategies", `{"name":"BTC Trend"}`), toolCall("c2", "GET", "/api/strategies/s1", "{}"), - toolCall("c3", "POST", "/api/traders", `{"name":"BTC趋势交易员","strategy_id":"s1"}`), + toolCall("c3", "POST", "/api/traders", `{"name":"BTC Trend Trader","strategy_id":"s1"}`), toolCall("c4", "POST", "/api/traders/tr1/start", "{}"), - textReply("策略和交易员已创建并启动!BTC趋势交易员正在运行。"), + textReply("Strategy and trader created and started! BTC Trend Trader is running."), }} a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt) - reply := a.Run("帮我配置个btc趋势交易的策略交易 跑起来", nil) + reply := a.Run("set up a BTC trend trading strategy and start it", nil) if llm.calls != 5 { t.Fatalf("expected 5 LLM calls, got %d", llm.calls) @@ -370,15 +370,15 @@ func TestStartExistingTrader(t *testing.T) { llm := &mockLLM{responses: []*mcp.LLMResponse{ toolCall("c1", "GET", "/api/my-traders", "{}"), toolCall("c2", "POST", "/api/traders/tr1/start", "{}"), - textReply("交易员 BTC Trader 已启动。"), + textReply("Trader BTC Trader has been started."), }} a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt) - reply := a.Run("启动交易员", nil) + reply := a.Run("start the trader", nil) if calls["POST /api/traders/tr1/start"] != 1 { t.Errorf("expected trader to be started, got %d start calls", calls["POST /api/traders/tr1/start"]) } - if reply != "交易员 BTC Trader 已启动。" { + if reply != "Trader BTC Trader has been started." { t.Fatalf("unexpected reply: %q", reply) } } diff --git a/telegram/agent/manager.go b/telegram/agent/manager.go index 90ee9322..760d3543 100644 --- a/telegram/agent/manager.go +++ b/telegram/agent/manager.go @@ -45,7 +45,7 @@ func (m *Manager) Run(chatID int64, userMessage string, onChunk func(string)) st case lane <- struct{}{}: case <-time.After(60 * time.Second): logger.Warnf("Agent: lane wait timeout for chat %d — previous message still processing", chatID) - return "Previous message is still being processed. Please wait a moment and try again. / 上一条消息仍在处理中,请稍等片刻后再试。" + return "Previous message is still being processed. Please wait a moment and try again." } defer func() { <-lane }() return a.Run(userMessage, onChunk) diff --git a/telegram/agent/prompt.go b/telegram/agent/prompt.go index c54bf5ee..478ffd49 100644 --- a/telegram/agent/prompt.go +++ b/telegram/agent/prompt.go @@ -36,7 +36,7 @@ The Account State block at the start of this conversation lists every resource w Read the id field from there and copy it verbatim — do not abbreviate, shorten, or guess. ## Behavior Rules -1. Reply in the same language the user used (中文→中文, English→English) +1. Reply in the same language the user used (match the user's language exactly) 2. Keep final replies concise — show results, not process 3. Ask for ALL missing required info in ONE message — never ask one field at a time 4. When user provides enough info, act immediately — no confirmation needed @@ -77,7 +77,7 @@ Use this to: - Only include "config" when user explicitly requests custom settings (specific coins, custom leverage, different timeframes). - After POST: GET /api/strategies/:id to verify → show user: name, coin_source.source_type, key risk_control values -**"帮我配置策略并跑起来" / "create strategy and start" (full setup workflow)**: +**"create strategy and start" (full setup workflow)**: Execute these steps IN ORDER with NO user confirmation between them: 1. POST /api/strategies — body: {"name":""} — no config needed, defaults are complete 2. GET /api/strategies/:id — verify strategy was saved diff --git a/telegram/bot.go b/telegram/bot.go index 6cf81dc6..72c9557f 100644 --- a/telegram/bot.go +++ b/telegram/bot.go @@ -355,12 +355,12 @@ func statusMsg(st *store.Store, userID string, apiPort int, lang string) string missing := "" if lang == "zh" { if !hasModel { - missing += "\n❌ AI 模型 → 设置 → AI 模型 → 添加" + missing += "\n❌ AI Model → Settings → AI Models → Add" } if !hasExchange { - missing += "\n❌ 交易所 → 设置 → 交易所 → 添加" + missing += "\n❌ Exchange → Settings → Exchanges → Add" } - return "⚙️ *需要完成初始配置*\n\n打开 Web 管理界面完成配置:\n→ " + webURL + "\n" + missing + "\n\n配置完成后发送 /start" + return "⚙️ *Setup required*\n\nOpen the web dashboard to complete setup:\n→ " + webURL + "\n" + missing + "\n\nSend /start when done." } if !hasModel { missing += "\n❌ AI Model → Settings → AI Models → Add" @@ -373,16 +373,16 @@ func statusMsg(st *store.Store, userID string, apiPort int, lang string) string // All configured — show ready state. if lang == "zh" { - return `✅ *NOFX 就绪,开始交易吧!* + return `✅ *NOFX is ready!* -直接告诉我你想做什么: +Just tell me what you want: -📊 "查看我的持仓" -💰 "账户余额多少" -🤖 "帮我创建 BTC 趋势策略并启动" -⏹ "停止所有交易员" +📊 "Show my positions" +💰 "What's my balance?" +🤖 "Create a BTC trend strategy and start it" +⏹ "Stop all traders" -/help 查看更多 · /lang 切换语言` +/help for more · /lang to change language` } return `✅ *NOFX is ready!* @@ -399,14 +399,14 @@ Just tell me what you want: // ── Language ────────────────────────────────────────────────────────────────── func langMenuMsg() string { - return "🌐 *Choose your language*\n\n1 — English\n2 — 中文\n\nReply with 1 or 2" + return "🌐 *Choose your language*\n\n1 — English\n2 — Chinese\n\nReply with 1 or 2" } func parseLangChoice(text string) string { switch strings.TrimSpace(text) { case "1", "en", "EN", "English", "english": return "en" - case "2", "zh", "ZH", "中文", "chinese", "Chinese": + case "2", "zh", "ZH", "chinese", "Chinese": return "zh" } return "" @@ -416,26 +416,26 @@ func parseLangChoice(text string) string { func helpMsg(lang string) string { if lang == "zh" { - return `*NOFX 使用指南* + return `*NOFX Help* -*查询* -• "查看我的持仓" -• "账户余额多少" -• "列出我的交易员" +*Query* +• "Show my positions" +• "What's my balance?" +• "List my traders" -*创建 & 启动* -• "帮我创建 BTC 趋势策略并跑起来" -• "保守型策略,只交易 BTC 和 ETH" +*Create & start* +• "Create a BTC trend strategy and start it" +• "Conservative strategy, BTC and ETH only" -*控制* -• "启动交易员" -• "暂停交易员" -• "停止所有交易" +*Control* +• "Start trader" +• "Pause trader" +• "Stop all trading" -*命令* -/start — 刷新状态 -/lang — 切换语言 -/help — 帮助` +*Commands* +/start — refresh status +/lang — change language +/help — show this` } return `*NOFX Help* diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 9a0fc8f2..dce878c5 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -418,6 +418,16 @@ func (at *AutoTrader) reloadStrategyConfigIfChanged() error { } strategyConfig.ClampLimits() + // Autopilot (vergex_signal/claw402) runs a balanced multi-position book: + // hold several instruments at once with a smaller per-position notional so + // multiple long/short positions fit the margin. Applied after ClampLimits so + // the book size is not capped back down to the conservative default. + if strategyConfig.CoinSource.SourceType == "vergex_signal" { + strategyConfig.RiskControl.MaxPositions = 6 + strategyConfig.RiskControl.BTCETHMaxPositionValueRatio = 1.2 + strategyConfig.RiskControl.AltcoinMaxPositionValueRatio = 1.2 + } + claw402Key := at.config.Claw402WalletKey if claw402Key == "" && at.config.AIModel == "claw402" && at.config.CustomAPIKey != "" { claw402Key = at.config.CustomAPIKey diff --git a/trader/auto_trader_force.go b/trader/auto_trader_force.go new file mode 100644 index 00000000..bc491566 --- /dev/null +++ b/trader/auto_trader_force.go @@ -0,0 +1,99 @@ +package trader + +import ( + "strings" + + "nofx/kernel" +) + +// ensureLongShortCoverage keeps a balanced book each cycle: it fills toward +// roughly half the MaxPositions slots long and half short. The AI still drives +// selection/sizing whenever it acts; this is a deterministic top-up — if the +// AI's decisions plus existing positions fall short of the per-direction target, +// the engine force-opens the strongest unused bullish/bearish candidates to +// reach it (never exceeding MaxPositions). +// +// Forced opens are sized from account equity via applyAutopilotFullSizeOpen and +// run through the same code-enforced risk checks (position-value ratio, minimum +// size, margin) as any other open. Guards: +// - skipped entirely in safe mode (AI unhealthy), +// - scoped to the vergex_signal source (the only one with directional bias), +// - never exceeds MaxPositions, +// - never doubles a base symbol already held or already in the decision set. +func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *kernel.Context, equity float64) []kernel.Decision { + if at == nil || ctx == nil || at.safeMode { + return decisions + } + if at.config.StrategyConfig == nil || at.config.StrategyConfig.CoinSource.SourceType != "vergex_signal" { + return decisions + } + if at.strategyEngine == nil || equity <= 0 { + return decisions + } + + maxPos := at.config.StrategyConfig.RiskControl.MaxPositions + if maxPos < 2 { + return decisions + } + // Aim to hold a balanced book: roughly half the slots long, half short. + targetLong := (maxPos + 1) / 2 + targetShort := maxPos / 2 + + held := make(map[string]bool) + longCount, shortCount, posCount := 0, 0, 0 + for _, p := range ctx.Positions { + held[universeBaseKey(p.Symbol)] = true + posCount++ + if strings.EqualFold(p.Side, "long") { + longCount++ + } else if strings.EqualFold(p.Side, "short") { + shortCount++ + } + } + for _, d := range decisions { + held[universeBaseKey(d.Symbol)] = true + switch d.Action { + case "open_long": + longCount++ + posCount++ + case "open_short": + shortCount++ + posCount++ + } + } + + bullish, bearish := at.strategyEngine.DirectionalCandidates() + + // fill a direction up to its target, drawing from the strongest unused + // candidates, never exceeding MaxPositions. + fill := func(action string, cands []string, have, target int) { + for _, c := range cands { + if have >= target { + return + } + if maxPos > 0 && posCount >= maxPos { + return + } + b := universeBaseKey(c) + if b == "" || held[b] { + continue + } + d := kernel.Decision{ + Action: action, + Symbol: c, + Confidence: 70, + Reasoning: "Forced " + action + " to fill the balanced long/short book (autopilot)", + } + at.applyAutopilotFullSizeOpen(&d, equity) + decisions = append(decisions, d) + held[b] = true + have++ + posCount++ + at.logInfof("⚖️ Forced %s %s (account-sized %.2f USDT, %dx)", action, c, d.PositionSizeUSD, d.Leverage) + } + } + + fill("open_long", bullish, longCount, targetLong) + fill("open_short", bearish, shortCount, targetShort) + return decisions +} diff --git a/trader/auto_trader_force_test.go b/trader/auto_trader_force_test.go new file mode 100644 index 00000000..55e10f25 --- /dev/null +++ b/trader/auto_trader_force_test.go @@ -0,0 +1,57 @@ +package trader + +import ( + "testing" + + "nofx/kernel" + "nofx/store" +) + +func baseForceTrader() *AutoTrader { + cfg := store.GetDefaultStrategyConfig("en") + cfg.CoinSource.SourceType = "vergex_signal" + cfg.RiskControl.MaxPositions = 5 + cfg.RiskControl.AltcoinMaxLeverage = 10 + cfg.RiskControl.AltcoinMaxPositionValueRatio = 10 + at := &AutoTrader{config: AutoTraderConfig{StrategyConfig: &cfg}} + at.strategyEngine = kernel.NewStrategyEngine(&cfg) // empty ranking cache + return at +} + +func TestEnsureLongShortCoverageSafeModeSkips(t *testing.T) { + at := baseForceTrader() + at.safeMode = true + out := at.ensureLongShortCoverage(nil, &kernel.Context{}, 100) + if len(out) != 0 { + t.Fatalf("safe mode must not force opens, got %d", len(out)) + } +} + +func TestEnsureLongShortCoverageNonVergexSkips(t *testing.T) { + at := baseForceTrader() + at.config.StrategyConfig.CoinSource.SourceType = "static" + out := at.ensureLongShortCoverage(nil, &kernel.Context{}, 100) + if len(out) != 0 { + t.Fatalf("non-vergex source must not force opens, got %d", len(out)) + } +} + +func TestEnsureLongShortCoverageBothPresentNoop(t *testing.T) { + at := baseForceTrader() + in := []kernel.Decision{ + {Action: "open_long", Symbol: "xyz:AAPL"}, + {Action: "open_short", Symbol: "BTC"}, + } + out := at.ensureLongShortCoverage(in, &kernel.Context{}, 100) + if len(out) != 2 { + t.Fatalf("both directions already present -> no force, got %d", len(out)) + } +} + +func TestEnsureLongShortCoverageNoCandidatesNoForce(t *testing.T) { + at := baseForceTrader() // empty ranking cache -> no directional candidates + out := at.ensureLongShortCoverage(nil, &kernel.Context{}, 100) + if len(out) != 0 { + t.Fatalf("no candidates available -> nothing to force, got %d", len(out)) + } +} diff --git a/trader/auto_trader_loop.go b/trader/auto_trader_loop.go index a483d11c..b19db8ab 100644 --- a/trader/auto_trader_loop.go +++ b/trader/auto_trader_loop.go @@ -222,6 +222,9 @@ func (at *AutoTrader) runCycle() error { // 8. Sort decisions: ensure close positions first, then open positions (prevent position stacking overflow) sortedDecisions := sortDecisionsByPriority(aiDecision.Decisions) sortedDecisions = at.filterDecisionsToStrategyUniverse(sortedDecisions, ctx) + // Per-cycle long/short coverage: if the AI left a direction uncovered, force + // the strongest bullish/bearish candidate (account-sized, risk-enforced). + sortedDecisions = at.ensureLongShortCoverage(sortedDecisions, ctx, ctx.Account.TotalEquity) logger.Info("🔄 Execution order (optimized): Close positions first → Open positions later") for i, d := range sortedDecisions { diff --git a/trader/auto_trader_throttle.go b/trader/auto_trader_throttle.go index 5771f230..b9db3c8f 100644 --- a/trader/auto_trader_throttle.go +++ b/trader/auto_trader_throttle.go @@ -12,9 +12,15 @@ import ( const ( autopilotMinHoldDuration = 45 * time.Minute autopilotNoiseCloseHoldDuration = 90 * time.Minute - autopilotReentryCooldown = 90 * time.Minute - autopilotMaxOpensPerHour = 1 - autopilotMaxOpensPerCycle = 1 + autopilotReentryCooldown = 30 * time.Minute + // Allow one long + one short per cycle. The real exposure/churn limits are + // MaxPositions (concurrent) + the 45m min-hold + the 90m per-symbol reentry + // cooldown, so the per-hour cap only needs to be high enough not to block the + // directional pair from re-establishing after positions close. A tight value + // here (e.g. 2) starves the strategy: once a couple opens fire, every later + // cycle is blocked and the book drains to flat. Keep it generous. + autopilotMaxOpensPerHour = 30 + autopilotMaxOpensPerCycle = 6 earlyCloseStopLossBypassPct = -2.5 earlyCloseTakeProfitBypassPct = 5.0 noiseCloseLossFloorPct = -1.0 diff --git a/trader/auto_trader_throttle_test.go b/trader/auto_trader_throttle_test.go index 2c7717dd..28256ee5 100644 --- a/trader/auto_trader_throttle_test.go +++ b/trader/auto_trader_throttle_test.go @@ -60,13 +60,29 @@ func TestTradeThrottleAllowsConfirmedLossAfterMinimumHold(t *testing.T) { } } -func TestTradeThrottleBlocksSecondOpenInCycle(t *testing.T) { +func TestTradeThrottleAllowsLongShortPairInCycle(t *testing.T) { at := &AutoTrader{} ctx := &kernel.Context{} - reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 1) - if !strings.Contains(reason, "only 1 new position") { - t.Fatalf("expected second open in cycle to be blocked, got %q", reason) + // One open already queued this cycle (e.g. the long) — the second open + // (the short) must still be allowed so a directional pair can open. + reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_short"}, ctx, 1) + if reason != "" { + t.Fatalf("expected the second (short) open in cycle to be allowed, got %q", reason) + } +} + +func TestTradeThrottleBlocksOpensOverCycleCap(t *testing.T) { + at := &AutoTrader{} + ctx := &kernel.Context{} + + // under the 6-per-cycle cap, a further open is allowed + if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 5); reason != "" { + t.Fatalf("expected open within the 6-per-cycle cap to be allowed, got %q", reason) + } + // at the cap, the next open is blocked + if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 6); !strings.Contains(reason, "6 new position") { + t.Fatalf("expected open beyond the 6-per-cycle cap to be blocked, got %q", reason) } } diff --git a/trader/hyperliquid/builder_fee_test.go b/trader/hyperliquid/builder_fee_test.go index 80c97fae..e11757ec 100644 --- a/trader/hyperliquid/builder_fee_test.go +++ b/trader/hyperliquid/builder_fee_test.go @@ -9,7 +9,7 @@ func TestDefaultBuilderIsHardcodedToApprovedFeeTier(t *testing.T) { if got := defaultBuilder.Builder; got != "0x891dc6f05ad47a3c1a05da55e7a7517971faaf0d" { t.Fatalf("defaultBuilder.Builder = %s, want hardcoded NOFX builder", got) } - // Fee is in tenths of a basis point: 50 = 5 bps = 0.05% (万5). + // Fee is in tenths of a basis point: 50 = 5 bps = 0.05% (5 per 10,000). // Must match defaultHyperliquidBuilderMaxFee on the API side and the // frontend HYPERLIQUID_BUILDER_MAX_FEE constant the user signs against. if got := defaultBuilder.Fee; got != 50 { diff --git a/trader/hyperliquid/trader.go b/trader/hyperliquid/trader.go index 3214de5a..4f463e1c 100644 --- a/trader/hyperliquid/trader.go +++ b/trader/hyperliquid/trader.go @@ -63,7 +63,7 @@ var xyzDexAssets = map[string]bool{ // Users approve this builder during the top-right Hyperliquid connect flow before // their generated agent wallet is saved for live trading. // -// Fee is in tenths of a basis point: 50 = 5 bps = 0.05% (万5). Existing +// Fee is in tenths of a basis point: 50 = 5 bps = 0.05% (5 per 10,000). Existing // approvals at the prior 0.1% cap remain valid on-chain because 0.05% is // still within their approved max. var defaultBuilder = &hyperliquid.BuilderInfo{ diff --git a/web/src/components/auth/LoginPage.tsx b/web/src/components/auth/LoginPage.tsx index 33ebe467..e471782d 100644 --- a/web/src/components/auth/LoginPage.tsx +++ b/web/src/components/auth/LoginPage.tsx @@ -70,14 +70,14 @@ export function LoginPage() {
{/* ───────── LEFT: brand panel (desktop only) ───────── */}
- {/* Ambient gold halo */} + {/* Ambient vermilion halo */}
{/* Brand mark */}
NOFX -
+
NOFX.
@@ -90,33 +90,33 @@ export function LoginPage() { Terminal Online
-

+

{language === 'zh' ? ( <> - AI 驱动的
- - 多市场交易终端 + AI-Powered
+ + Multi-Market Trading Terminal ) : language === 'id' ? ( <> Terminal Trading
- + Multi-Pasar AI ) : ( <> AI-Powered
- + Trading Terminal )}

-

+

{language === 'zh' - ? '一键接入 Hyperliquid、OKX、Aster 等 10+ 交易所与 7 个 LLM 模型, 用自然语言部署 24/7 自动化策略.' + ? 'Plug into 10+ exchanges including Hyperliquid, OKX, Aster, and 7 LLM models. Deploy 24/7 automated strategies with natural language.' : language === 'id' ? 'Hubungkan ke 10+ bursa termasuk Hyperliquid, OKX, Aster dan 7 model LLM. Terapkan strategi otomatis 24/7 dengan bahasa alami.' : 'Plug into 10+ exchanges including Hyperliquid, OKX, Aster, and 7 LLM models. Deploy 24/7 automated strategies with natural language.'} @@ -129,7 +129,7 @@ export function LoginPage() { value="10+" label={ language === 'zh' - ? '交易所' + ? 'Exchanges' : language === 'id' ? 'Bursa' : 'Exchanges' @@ -139,7 +139,7 @@ export function LoginPage() { value="7" label={ language === 'zh' - ? 'AI 模型' + ? 'AI Models' : language === 'id' ? 'Model AI' : 'AI Models' @@ -149,7 +149,7 @@ export function LoginPage() { value="24/7" label={ language === 'zh' - ? '全天候' + ? 'Always On' : language === 'id' ? 'Sepanjang Waktu' : 'Always On' @@ -164,19 +164,19 @@ export function LoginPage() { {/* Mobile brand */}

NOFX -
+
NOFX.
{/* Form header */}
-

+

{t('signIn', language)}

-

+

{language === 'zh' - ? '使用您的邮箱继续' + ? 'Continue with your email' : language === 'id' ? 'Lanjutkan dengan email Anda' : 'Continue with your email'} @@ -187,14 +187,14 @@ export function LoginPage() {

{/* Email */}
-
))} diff --git a/web/src/components/faq/FAQLayout.tsx b/web/src/components/faq/FAQLayout.tsx index cd70e31a..af104b67 100644 --- a/web/src/components/faq/FAQLayout.tsx +++ b/web/src/components/faq/FAQLayout.tsx @@ -63,11 +63,11 @@ export function FAQLayout({ language }: FAQLayoutProps) { {/* Page Header */}
-
- +
+
-

+

{t('faqTitle', language)}

@@ -80,7 +80,7 @@ export function FAQLayout({ language }: FAQLayoutProps) { searchTerm={searchTerm} onSearchChange={setSearchTerm} placeholder={ - language === 'zh' ? '搜索常见问题...' : 'Search FAQ...' + language === 'zh' ? 'Search FAQ...' : 'Search FAQ...' } />

@@ -108,21 +108,20 @@ export function FAQLayout({ language }: FAQLayoutProps) { /> ) : (
-

+

{language === 'zh' - ? '没有找到匹配的问题' + ? 'No matching questions found' : 'No matching questions found'}

)} @@ -133,15 +132,14 @@ export function FAQLayout({ language }: FAQLayoutProps) {
-

+

{t('faqStillHaveQuestions', language)}

-

+

{t('faqContactUs', language)}

@@ -151,9 +149,9 @@ export function FAQLayout({ language }: FAQLayoutProps) { rel="noopener noreferrer" className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105" style={{ - background: '#1E2329', - color: '#EAECEF', - border: '1px solid #2B3139', + background: '#F7F4EC', + color: '#1A1813', + border: '1px solid rgba(26,24,19,0.14)', }} > GitHub @@ -164,8 +162,8 @@ export function FAQLayout({ language }: FAQLayoutProps) { rel="noopener noreferrer" className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105" style={{ - background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)', - color: '#0B0E11', + background: '#E0483B', + color: '#F1ECE2', }} > {t('community', language)} diff --git a/web/src/components/faq/FAQSearchBar.tsx b/web/src/components/faq/FAQSearchBar.tsx index d3d33a5a..e5b4d048 100644 --- a/web/src/components/faq/FAQSearchBar.tsx +++ b/web/src/components/faq/FAQSearchBar.tsx @@ -21,12 +21,12 @@ export function FAQSearchBar({ value={searchTerm} onChange={(e) => onSearchChange(e.target.value)} placeholder={placeholder} - className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none bg-black/40 border border-white/10 text-nofx-text-main placeholder-nofx-text-muted/50 focus:border-nofx-gold/50 focus:ring-1 focus:ring-nofx-gold/20 hover:border-nofx-gold/30 font-mono" + className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] text-nofx-text placeholder-nofx-text-muted/50 focus:border-nofx-gold/50 focus:ring-1 focus:ring-nofx-gold/20 hover:border-nofx-gold/30 font-mono" /> {searchTerm && ( diff --git a/web/src/components/faq/FAQSidebar.tsx b/web/src/components/faq/FAQSidebar.tsx index 27ff0871..3eebb5a8 100644 --- a/web/src/components/faq/FAQSidebar.tsx +++ b/web/src/components/faq/FAQSidebar.tsx @@ -19,12 +19,12 @@ export function FAQSidebar({ className="sticky top-24 h-[calc(100vh-120px)] overflow-y-auto pr-4" style={{ scrollbarWidth: 'thin', - scrollbarColor: '#2B3139 #1E2329', + scrollbarColor: '#E8E2D5 #F7F4EC', }} >
{categories.map((category) => ( -
+
{/* Category Title */}
@@ -43,7 +43,7 @@ export function FAQSidebar({ onClick={() => onItemClick(category.id, item.id)} className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-all border-l-[3px] ${isActive ? 'bg-nofx-gold/10 text-nofx-gold border-nofx-gold pl-[9px]' - : 'bg-transparent text-nofx-text-muted border-transparent pl-3 hover:bg-nofx-gold/5 hover:text-nofx-text-main' + : 'bg-transparent text-nofx-text-muted border-transparent pl-3 hover:bg-nofx-gold/5 hover:text-nofx-text' }`} > {t(item.questionKey, language)} diff --git a/web/src/components/landing/AboutSection.tsx b/web/src/components/landing/AboutSection.tsx index 369f8171..17d34c67 100644 --- a/web/src/components/landing/AboutSection.tsx +++ b/web/src/components/landing/AboutSection.tsx @@ -10,27 +10,27 @@ export default function AboutSection({ language }: AboutSectionProps) { const features = [ { icon: Shield, - title: language === 'zh' ? '完全自主控制' : 'Full Control', - desc: language === 'zh' ? '自托管,数据安全' : 'Self-hosted, data secure', + title: language === 'zh' ? 'Full Control' : 'Full Control', + desc: language === 'zh' ? 'Self-hosted, data secure' : 'Self-hosted, data secure', }, { icon: Cpu, - title: language === 'zh' ? '多 AI 支持' : 'Multi-AI Support', + title: language === 'zh' ? 'Multi-AI Support' : 'Multi-AI Support', desc: language === 'zh' ? 'DeepSeek, GPT, Claude...' : 'DeepSeek, GPT, Claude...', }, { icon: BarChart3, - title: language === 'zh' ? '实时监控' : 'Real-time Monitor', - desc: language === 'zh' ? '可视化交易看板' : 'Visual trading dashboard', + title: language === 'zh' ? 'Real-time Monitor' : 'Real-time Monitor', + desc: language === 'zh' ? 'Visual trading dashboard' : 'Visual trading dashboard', }, ] return ( -
+
{/* Background Decoration */}
@@ -45,21 +45,21 @@ export default function AboutSection({ language }: AboutSectionProps) { - - + + {t('aboutNofx', language)} -

+

{t('whatIsNofx', language)}

-

+

{t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)}

@@ -74,21 +74,21 @@ export default function AboutSection({ language }: AboutSectionProps) { transition={{ delay: index * 0.1 }} className="flex items-center gap-3 px-4 py-3 rounded-xl" style={{ - background: 'rgba(255, 255, 255, 0.03)', - border: '1px solid rgba(255, 255, 255, 0.06)', + background: '#F7F4EC', + border: '1px solid rgba(26, 24, 19, 0.14)', }} >
- +
-
+
{feature.title}
-
+
{feature.desc}
@@ -107,36 +107,36 @@ export default function AboutSection({ language }: AboutSectionProps) {
{/* Terminal Header */}
-
-
-
+
+
+
- terminal + terminal
{/* Terminal Content */}
-
$ git clone https://github.com/NoFxAiOS/nofx.git
-
$ cd nofx && chmod +x start.sh
-
$ ./start.sh start --build
-
+
$ git clone https://github.com/NoFxAiOS/nofx.git
+
$ cd nofx && chmod +x start.sh
+
$ ./start.sh start --build
+
✓ {t('startupMessages1', language)}
-
+
✓ {t('startupMessages2', language)}
-
+
✓ {t('startupMessages3', language)}
- - _ + + _
diff --git a/web/src/components/landing/AnimatedSection.tsx b/web/src/components/landing/AnimatedSection.tsx index 6815efcf..e2dd7ef2 100644 --- a/web/src/components/landing/AnimatedSection.tsx +++ b/web/src/components/landing/AnimatedSection.tsx @@ -4,7 +4,7 @@ import { motion, useInView } from 'framer-motion' export default function AnimatedSection({ children, id, - backgroundColor = 'var(--brand-black)', + backgroundColor = '#F1ECE2', }: { children: React.ReactNode id?: string diff --git a/web/src/components/landing/CommunitySection.tsx b/web/src/components/landing/CommunitySection.tsx index b7ff9a00..1403b8b6 100644 --- a/web/src/components/landing/CommunitySection.tsx +++ b/web/src/components/landing/CommunitySection.tsx @@ -19,8 +19,8 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw rel="noopener noreferrer" className="block p-5 rounded-2xl transition-all duration-300 group" style={{ - background: '#12161C', - border: '1px solid rgba(255, 255, 255, 0.06)', + background: '#F7F4EC', + border: '1px solid rgba(26, 24, 19, 0.14)', }} initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} @@ -28,7 +28,7 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw transition={{ delay }} whileHover={{ y: -4, - borderColor: 'rgba(240, 185, 11, 0.3)', + borderColor: 'rgba(224, 72, 59, 0.3)', }} > {/* Header */} @@ -38,13 +38,13 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw src={avatarUrl} alt={authorName} className="w-10 h-10 rounded-full object-cover" - style={{ border: '2px solid rgba(255, 255, 255, 0.1)' }} + style={{ border: '2px solid rgba(26, 24, 19, 0.14)' }} />
-
+
{authorName}
-
+
{handle}
@@ -52,7 +52,7 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw {/* X Logo */}
@@ -63,27 +63,27 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw {/* Content */}

{quote}

{/* Footer */} -
-
+
+
Reply
-
+
Repost
-
+
Like
- +
@@ -98,11 +98,11 @@ export default function CommunitySection({ language }: CommunitySectionProps) { const tweets: TweetProps[] = [] return ( -
+
{/* Background Decoration */}
@@ -113,11 +113,11 @@ export default function CommunitySection({ language }: CommunitySectionProps) { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} > -

- {language === 'zh' ? '社区声音' : 'Community Voices'} +

+ {language === 'zh' ? 'Community Voices' : 'Community Voices'}

-

- {language === 'zh' ? '看看大家怎么说' : 'See what others are saying'} +

+ {language === 'zh' ? 'See what others are saying' : 'See what others are saying'}

@@ -141,15 +141,15 @@ export default function CommunitySection({ language }: CommunitySectionProps) { rel="noopener noreferrer" className="inline-flex items-center gap-2 px-6 py-3 rounded-xl font-medium transition-all hover:scale-105" style={{ - background: 'rgba(29, 161, 242, 0.1)', - color: '#1DA1F2', - border: '1px solid rgba(29, 161, 242, 0.3)', + background: 'rgba(224, 72, 59, 0.1)', + color: '#E0483B', + border: '1px solid rgba(224, 72, 59, 0.3)', }} > - {language === 'zh' ? '关注我们的 X' : 'Follow us on X'} + {language === 'zh' ? 'Follow us on X' : 'Follow us on X'}
diff --git a/web/src/components/landing/FeaturesSection.tsx b/web/src/components/landing/FeaturesSection.tsx index 55bcbeb1..bc26112a 100644 --- a/web/src/components/landing/FeaturesSection.tsx +++ b/web/src/components/landing/FeaturesSection.tsx @@ -10,61 +10,61 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) { const features = [ { icon: Brain, - title: language === 'zh' ? 'AI 策略编排引擎' : 'AI Strategy Orchestration', + title: language === 'zh' ? 'AI Strategy Orchestration' : 'AI Strategy Orchestration', desc: language === 'zh' - ? '支持 DeepSeek、GPT、Claude、Qwen 等多种大模型,自定义 Prompt 策略,AI 自主分析市场并做出交易决策' + ? 'Support DeepSeek, GPT, Claude, Qwen and more. Custom prompts, AI autonomously analyzes markets and makes trading decisions' : 'Support DeepSeek, GPT, Claude, Qwen and more. Custom prompts, AI autonomously analyzes markets and makes trading decisions', highlight: true, - badge: language === 'zh' ? '核心能力' : 'Core', + badge: language === 'zh' ? 'Core' : 'Core', }, { icon: Swords, - title: language === 'zh' ? '多 AI 竞技场' : 'Multi-AI Arena', + title: language === 'zh' ? 'Multi-AI Arena' : 'Multi-AI Arena', desc: language === 'zh' - ? '多个 AI 交易员同台竞技,实时 PnL 排行榜,自动优胜劣汰,让最强策略脱颖而出' + ? 'Multiple AI traders compete in real-time, live PnL leaderboard, automatic survival of the fittest' : 'Multiple AI traders compete in real-time, live PnL leaderboard, automatic survival of the fittest', highlight: true, - badge: language === 'zh' ? '独创' : 'Unique', + badge: language === 'zh' ? 'Unique' : 'Unique', }, { icon: LineChart, - title: language === 'zh' ? '专业量化数据' : 'Pro Quant Data', + title: language === 'zh' ? 'Pro Quant Data' : 'Pro Quant Data', desc: language === 'zh' - ? '集成 K线、技术指标、市场深度、资金费率、持仓量等专业量化数据,为 AI 决策提供全面信息' + ? 'Integrated candlesticks, indicators, order book, funding rates, open interest - comprehensive data for AI decisions' : 'Integrated candlesticks, indicators, order book, funding rates, open interest - comprehensive data for AI decisions', highlight: true, - badge: language === 'zh' ? '专业' : 'Pro', + badge: language === 'zh' ? 'Pro' : 'Pro', }, { icon: Blocks, - title: language === 'zh' ? '多交易所支持' : 'Multi-Exchange Support', + title: language === 'zh' ? 'Multi-Exchange Support' : 'Multi-Exchange Support', desc: language === 'zh' - ? 'Binance、OKX、Bybit、Hyperliquid、Aster DEX,一套系统管理多个交易所' + ? 'Binance, OKX, Bybit, Hyperliquid, Aster DEX - one system, multiple exchanges' : 'Binance, OKX, Bybit, Hyperliquid, Aster DEX - one system, multiple exchanges', }, { icon: BarChart3, - title: language === 'zh' ? '实时可视化看板' : 'Real-time Dashboard', + title: language === 'zh' ? 'Real-time Dashboard' : 'Real-time Dashboard', desc: language === 'zh' - ? '交易监控、收益曲线、持仓分析、AI 决策日志,一目了然' + ? 'Trade monitoring, PnL curves, position analysis, AI decision logs at a glance' : 'Trade monitoring, PnL curves, position analysis, AI decision logs at a glance', }, { icon: Shield, - title: language === 'zh' ? '开源自托管' : 'Open Source & Self-Hosted', + title: language === 'zh' ? 'Open Source & Self-Hosted' : 'Open Source & Self-Hosted', desc: language === 'zh' - ? '代码完全开源可审计,数据存储在本地,API 密钥不经过第三方' + ? 'Fully open source, data stored locally, API keys never leave your server' : 'Fully open source, data stored locally, API keys never leave your server', }, ] return ( -
+
{/* Background */}
@@ -77,12 +77,12 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) { whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} > -

+

{t('whyChooseNofx', language)}

-

+

{language === 'zh' - ? '不只是交易机器人,而是完整的 AI 交易操作系统' + ? 'Not just a trading bot, but a complete AI trading operating system' : 'Not just a trading bot, but a complete AI trading operating system'}

@@ -102,11 +102,11 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) { `} style={{ background: feature.highlight - ? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, rgba(240, 185, 11, 0.02) 100%)' - : '#12161C', + ? 'rgba(224, 72, 59, 0.06)' + : '#F7F4EC', border: feature.highlight - ? '1px solid rgba(240, 185, 11, 0.2)' - : '1px solid rgba(255, 255, 255, 0.06)', + ? '1px solid rgba(224, 72, 59, 0.2)' + : '1px solid rgba(26, 24, 19, 0.14)', }} > {/* Badge */} @@ -114,8 +114,8 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
{feature.badge} @@ -127,36 +127,36 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) { className="w-12 h-12 rounded-xl flex items-center justify-center mb-4" style={{ background: feature.highlight - ? 'rgba(240, 185, 11, 0.15)' - : 'rgba(240, 185, 11, 0.1)', - border: '1px solid rgba(240, 185, 11, 0.2)', + ? 'rgba(224, 72, 59, 0.15)' + : 'rgba(224, 72, 59, 0.1)', + border: '1px solid rgba(224, 72, 59, 0.2)', }} whileHover={{ scale: 1.1, rotate: 5 }} > {/* Text */}

{feature.title}

{feature.desc}

{/* Hover Glow */}
))} @@ -170,30 +170,28 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) { viewport={{ once: true }} > {[ - { value: '10+', label: language === 'zh' ? 'AI 模型支持' : 'AI Models' }, - { value: '5+', label: language === 'zh' ? '交易所集成' : 'Exchanges' }, - { value: '24/7', label: language === 'zh' ? '自动交易' : 'Auto Trading' }, - { value: '100%', label: language === 'zh' ? '开源免费' : 'Open Source' }, + { value: '10+', label: language === 'zh' ? 'AI Models' : 'AI Models' }, + { value: '5+', label: language === 'zh' ? 'Exchanges' : 'Exchanges' }, + { value: '24/7', label: language === 'zh' ? 'Auto Trading' : 'Auto Trading' }, + { value: '100%', label: language === 'zh' ? 'Open Source' : 'Open Source' }, ].map((stat) => (
{stat.value}
-
+
{stat.label}
diff --git a/web/src/components/landing/FooterSection.tsx b/web/src/components/landing/FooterSection.tsx index f09f373b..eb91d0b3 100644 --- a/web/src/components/landing/FooterSection.tsx +++ b/web/src/components/landing/FooterSection.tsx @@ -23,7 +23,7 @@ export default function FooterSection({ language }: FooterSectionProps) { ], resources: [ { - name: language === 'zh' ? '文档' : 'Documentation', + name: language === 'zh' ? 'Documentation' : 'Documentation', href: 'https://github.com/NoFxAiOS/nofx/blob/main/README.md', }, { name: 'Issues', href: 'https://github.com/NoFxAiOS/nofx/issues' }, @@ -43,7 +43,7 @@ export default function FooterSection({ language }: FooterSectionProps) { } return ( -