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
This commit is contained in:
tinkle-community
2026-06-30 16:03:52 +08:00
parent eba28bcf0e
commit 110bf52908
149 changed files with 6835 additions and 3611 deletions

View File

@@ -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%"

58
api/handler_stats_full.go Normal file
View File

@@ -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)
}

View File

@@ -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 UserSigner 和私钥是否正确"
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
}

View File

@@ -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",

View File

@@ -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)

View File

@@ -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 == "" {

View File

@@ -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=<EXACT trader_id from GET /api/my-traders>
Returns: {"total_trades":<int>,"winning_trades":<int>,"win_rate":<float>,"total_pnl":<float>,"sharpe_ratio":<float>,"max_drawdown":<float>}`,
s.handleStatistics)
s.routeWithSchema(protected, "GET", "/statistics/full", "Full trade-quality metrics (win rate, profit factor, Sharpe, max drawdown)",
`Query: ?trader_id=<EXACT trader_id from GET /api/my-traders>
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)
}
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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 标签 <reasoning> <decision> 分隔简明分析和决策 JSON\n\n")
sb.WriteString("方向必须由数据决定:上涨结构确认时可以 `open_long`,下跌结构确认时可以 `open_short`;不要默认只做多或只做空。\n\n")
sb.WriteString("Use XML tags <reasoning> and <decision> 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 <reasoning> and <decision> 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("<reasoning>\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("</decision>\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 标签 <reasoning> <decision> 分隔思维链和决策 JSON, 避免解析错误**\n\n")
sb.WriteString("**Must use XML tags <reasoning> and <decision> to separate chain of thought and decision JSON, avoiding parsing errors**\n\n")
} else {
sb.WriteString("**Must use XML tags <reasoning> and <decision> to separate chain of thought and decision JSON, avoiding parsing errors**\n\n")
}
sb.WriteString("## Format Requirements\n\n")
sb.WriteString("<reasoning>\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("</reasoning>\n\n")
sb.WriteString("<decision>\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("</decision>\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")

View File

@@ -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")

View File

@@ -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" {

View File

@@ -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]

View File

@@ -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 ==========

View File

@@ -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"),

View File

@@ -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

View File

@@ -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)

View File

@@ -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",
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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 != "" {

View File

@@ -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 RankingSignal LabCost/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{

View File

@@ -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()

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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":"<descriptive name>"} — no config needed, defaults are complete
2. GET /api/strategies/:id — verify strategy was saved

View File

@@ -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*

View File

@@ -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

View File

@@ -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
}

View File

@@ -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))
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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{

View File

@@ -70,14 +70,14 @@ export function LoginPage() {
<main className="flex-1 grid lg:grid-cols-2">
{/* ───────── LEFT: brand panel (desktop only) ───────── */}
<section className="hidden lg:flex flex-col justify-between p-12 xl:p-16 relative overflow-hidden">
{/* Ambient gold halo */}
{/* Ambient vermilion halo */}
<div className="absolute -left-32 top-1/3 w-[28rem] h-[28rem] bg-nofx-gold/[0.06] rounded-full blur-3xl pointer-events-none" />
<div className="absolute -right-16 bottom-0 w-72 h-72 bg-nofx-accent/[0.04] rounded-full blur-3xl pointer-events-none" />
{/* Brand mark */}
<div className="flex items-center gap-3 relative">
<img src="/icons/nofx.svg" alt="NOFX" className="w-9 h-9" />
<div className="font-mono font-bold text-xl tracking-tight text-white">
<div className="font-mono font-bold text-xl tracking-tight text-nofx-text">
NOFX<span className="text-nofx-gold">.</span>
</div>
</div>
@@ -90,33 +90,33 @@ export function LoginPage() {
Terminal Online
</span>
</div>
<h2 className="text-4xl xl:text-5xl font-bold tracking-tight text-white leading-[1.05]">
<h2 className="text-4xl xl:text-5xl font-bold tracking-tight text-nofx-text leading-[1.05]">
{language === 'zh' ? (
<>
AI <br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
AI-Powered<br />
<span className="text-nofx-gold">
Multi-Market Trading Terminal
</span>
</>
) : language === 'id' ? (
<>
Terminal Trading<br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
<span className="text-nofx-gold">
Multi-Pasar AI
</span>
</>
) : (
<>
AI-Powered<br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
<span className="text-nofx-gold">
Trading Terminal
</span>
</>
)}
</h2>
<p className="mt-5 text-zinc-400 text-base leading-relaxed max-w-md">
<p className="mt-5 text-nofx-text-muted text-base leading-relaxed max-w-md">
{language === 'zh'
? '一键接入 HyperliquidOKXAster 等 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 */}
<div className="lg:hidden flex flex-col items-center gap-3 mb-10">
<img src="/icons/nofx.svg" alt="NOFX" className="w-12 h-12" />
<div className="font-mono font-bold text-lg tracking-tight text-white">
<div className="font-mono font-bold text-lg tracking-tight text-nofx-text">
NOFX<span className="text-nofx-gold">.</span>
</div>
</div>
{/* Form header */}
<div className="mb-7">
<h1 className="text-[26px] sm:text-3xl font-bold tracking-tight text-white">
<h1 className="text-[26px] sm:text-3xl font-bold tracking-tight text-nofx-text">
{t('signIn', language)}
</h1>
<p className="mt-1.5 text-sm text-zinc-500">
<p className="mt-1.5 text-sm text-nofx-text-muted">
{language === 'zh'
? '使用您的邮箱继续'
? 'Continue with your email'
: language === 'id'
? 'Lanjutkan dengan email Anda'
: 'Continue with your email'}
@@ -187,14 +187,14 @@ export function LoginPage() {
<form onSubmit={handleLogin} className="space-y-4">
{/* Email */}
<div>
<label className="block text-[10.5px] font-medium uppercase tracking-[0.14em] text-zinc-500 mb-2">
<label className="block text-[10.5px] font-medium uppercase tracking-[0.14em] text-nofx-text-muted mb-2">
{t('email', language)}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-zinc-900/60 border border-white/[0.08] rounded-lg px-4 py-[11px] text-[14px] text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/50 focus:bg-zinc-900 focus:ring-2 focus:ring-nofx-gold/20 transition-all"
className="w-full bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-[11px] text-[14px] text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/50 focus:bg-nofx-bg-lighter focus:ring-2 focus:ring-nofx-gold/20 transition-all"
placeholder="you@example.com"
required
autoFocus
@@ -205,13 +205,13 @@ export function LoginPage() {
{/* Password */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-[10.5px] font-medium uppercase tracking-[0.14em] text-zinc-500">
<label className="text-[10.5px] font-medium uppercase tracking-[0.14em] text-nofx-text-muted">
{t('password', language)}
</label>
<button
type="button"
onClick={() => navigate('/reset-password')}
className="text-xs text-zinc-500 hover:text-nofx-gold transition-colors"
className="text-xs text-nofx-text-muted hover:text-nofx-gold transition-colors"
>
{t('forgotPassword', language)}
</button>
@@ -221,7 +221,7 @@ export function LoginPage() {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-zinc-900/60 border border-white/[0.08] rounded-lg px-4 py-[11px] pr-11 text-[14px] text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/50 focus:bg-zinc-900 focus:ring-2 focus:ring-nofx-gold/20 transition-all"
className="w-full bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-[11px] pr-11 text-[14px] text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/50 focus:bg-nofx-bg-lighter focus:ring-2 focus:ring-nofx-gold/20 transition-all"
placeholder="••••••••"
required
autoComplete="current-password"
@@ -229,7 +229,7 @@ export function LoginPage() {
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
@@ -239,8 +239,8 @@ export function LoginPage() {
{/* Error banner */}
{error && (
<div className="flex items-start gap-2 rounded-lg border border-red-500/25 bg-red-500/[0.08] px-3 py-2.5 text-xs text-red-300">
<span className="text-red-400 font-bold mt-px">!</span>
<div className="flex items-start gap-2 rounded-lg border border-nofx-danger/25 bg-nofx-danger/[0.08] px-3 py-2.5 text-xs text-nofx-danger">
<span className="text-nofx-danger font-bold mt-px">!</span>
<span className="leading-relaxed">{error}</span>
</div>
)}
@@ -249,7 +249,7 @@ export function LoginPage() {
<button
type="submit"
disabled={loading}
className="group mt-2 flex w-full items-center justify-center gap-2 rounded-lg bg-nofx-gold py-[11px] text-sm font-semibold text-black shadow-lg shadow-nofx-gold/10 transition-all hover:bg-yellow-400 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
className="group mt-2 flex w-full items-center justify-center gap-2 rounded-lg bg-nofx-gold py-[11px] text-sm font-semibold text-nofx-bg transition-all hover:bg-nofx-gold-highlight active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
>
{loading ? (
<>
@@ -269,12 +269,12 @@ export function LoginPage() {
</form>
{/* Footer */}
<div className="mt-8 pt-5 border-t border-white/[0.06] flex items-center justify-between text-[11px]">
<span className="font-mono text-zinc-600">v1.0</span>
<div className="mt-8 pt-5 border-t border-[rgba(26,24,19,0.14)] flex items-center justify-between text-[11px]">
<span className="font-mono text-nofx-text-muted">v1.0</span>
<button
type="button"
onClick={handleResetAccount}
className="text-zinc-600 transition-colors hover:text-red-400"
className="text-nofx-text-muted transition-colors hover:text-nofx-danger"
>
{t('forgotAccount', language)}
</button>
@@ -289,10 +289,10 @@ export function LoginPage() {
function Stat({ value, label }: { value: string; label: string }) {
return (
<div>
<div className="font-mono text-2xl xl:text-3xl font-bold text-white">
<div className="font-mono text-2xl xl:text-3xl font-bold text-nofx-text">
{value}
</div>
<div className="mt-1 text-[10.5px] uppercase tracking-[0.14em] text-zinc-500">
<div className="mt-1 text-[10.5px] uppercase tracking-[0.14em] text-nofx-text-muted">
{label}
</div>
</div>

View File

@@ -46,11 +46,11 @@ export function LoginRequiredOverlay({
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ type: 'spring', damping: 20, stiffness: 300 }}
className="relative max-w-md w-full overflow-hidden bg-nofx-bg border border-nofx-gold/30 shadow-neon rounded-sm group font-mono"
className="relative max-w-md w-full overflow-hidden bg-nofx-bg-lighter border border-nofx-gold/30 shadow-lg rounded-sm group font-mono"
onClick={(e) => e.stopPropagation()}
>
{/* Terminal Window Header */}
<div className="flex items-center justify-between px-3 py-2 bg-nofx-bg-lighter border-b border-nofx-gold/20">
<div className="flex items-center justify-between px-3 py-2 bg-nofx-bg-deeper border-b border-nofx-gold/20">
<div className="flex items-center gap-2">
<Terminal size={12} className="text-nofx-gold" />
<span className="text-[10px] text-nofx-text-muted uppercase tracking-wider">
@@ -74,8 +74,7 @@ export function LoginRequiredOverlay({
{/* Flashing Access Denied */}
<div className="flex justify-center mb-6">
<div className="relative">
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
<div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
<div className="bg-nofx-bg-lighter border border-nofx-danger/50 text-nofx-danger px-4 py-2 flex items-center gap-3">
<AlertTriangle size={18} className="animate-pulse" />
<span className="font-bold tracking-widest text-sm uppercase">
{tr('accessDenied')}
@@ -87,7 +86,7 @@ export function LoginRequiredOverlay({
{/* Terminal Text */}
<div className="space-y-4 mb-8">
<div className="text-center">
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">
<h2 className="text-xl font-bold text-nofx-text uppercase tracking-wider mb-2">
{tr('title')}
</h2>
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">
@@ -95,9 +94,9 @@ export function LoginRequiredOverlay({
</p>
</div>
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
<div className="bg-nofx-bg-deeper border-l-2 border-nofx-gold/20 p-3 my-4">
<p className="text-xs text-nofx-text-muted leading-relaxed font-mono">
<span className="text-green-500 mr-2">$</span>
<span className="text-nofx-success mr-2">$</span>
{tr('description')}
</p>
</div>
@@ -118,7 +117,7 @@ export function LoginRequiredOverlay({
<div className="space-y-3">
<Link
to="/login"
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-nofx-bg font-bold text-xs uppercase tracking-widest hover:bg-nofx-gold-highlight transition-all group"
>
<LogIn size={14} />
<span>{tr('loginButton')}</span>
@@ -129,7 +128,7 @@ export function LoginRequiredOverlay({
<Link
to="/register"
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-nofx-gold/20 text-nofx-text-muted hover:text-white hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10"
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-nofx-gold/20 text-nofx-text-muted hover:text-nofx-text hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10"
>
<UserPlus size={14} />
<span>{tr('registerButton')}</span>
@@ -139,7 +138,7 @@ export function LoginRequiredOverlay({
<div className="mt-4 text-center">
<button
onClick={onClose}
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-nofx-danger/30"
>
[ {tr('abort')} ]
</button>

View File

@@ -21,25 +21,25 @@ export function OnboardingModeSelector({
}> = [
{
id: 'beginner',
title: isZh ? '新手模式' : 'Beginner Mode',
badge: isZh ? '推荐' : 'Recommended',
title: isZh ? 'Beginner Mode' : 'Beginner Mode',
badge: isZh ? 'Recommended' : 'Recommended',
description: isZh
? '自动生成 Base 钱包,默认接入 Claw402 + GLM,最快完成首次启动。'
? 'Generate a Base wallet automatically and start with Claw402 + GLM by default.'
: 'Generate a Base wallet automatically and start with Claw402 + GLM by default.',
},
{
id: 'advanced',
title: isZh ? '老手模式' : 'Advanced Mode',
title: isZh ? 'Advanced Mode' : 'Advanced Mode',
description: isZh
? '保持现在的完整配置流程,你自己决定模型、钱包和交易所。'
? 'Keep the full manual flow and configure models, wallets, and exchanges yourself.'
: 'Keep the full manual flow and configure models, wallets, and exchanges yourself.',
},
]
return (
<div className="space-y-2">
<div className="text-xs font-medium text-zinc-400">
{isZh ? '使用模式' : 'Experience'}
<div className="text-xs font-medium text-nofx-text-muted">
{isZh ? 'Experience' : 'Experience'}
</div>
<div className="grid grid-cols-1 gap-2">
{options.map((option) => {
@@ -51,19 +51,19 @@ export function OnboardingModeSelector({
onClick={() => onChange(option.id)}
className={`w-full rounded-xl border px-4 py-3 text-left transition-all ${
selected
? 'border-nofx-gold/60 bg-nofx-gold/10 shadow-[0_0_0_1px_rgba(240,185,11,0.15)]'
: 'border-zinc-800 bg-zinc-950/60 hover:border-zinc-700'
? 'border-nofx-gold/60 bg-nofx-gold/10'
: 'border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter hover:border-nofx-gold/40'
}`}
>
<div className="flex items-center gap-2 text-sm font-semibold text-white">
<div className="flex items-center gap-2 text-sm font-semibold text-nofx-text">
<span>{option.title}</span>
{option.badge ? (
<span className="rounded-full bg-nofx-gold px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-black">
<span className="rounded-full bg-nofx-gold px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-nofx-bg">
{option.badge}
</span>
) : null}
</div>
<p className="mt-1 text-xs leading-5 text-zinc-400">
<p className="mt-1 text-xs leading-5 text-nofx-text-muted">
{option.description}
</p>
</button>

View File

@@ -1,25 +1,25 @@
import { describe, it, expect } from 'vitest'
/**
* PR #XXX 测试: 修复密码校验不一致的问题
* PR #XXX test: fix inconsistent password validation
*
* 问题:RegisterPage 中存在两处密码校验逻辑:
* 1. PasswordChecklist 组件提供的可视化校验
* 2. 自定义的 isStrongPassword 函数
* 这导致校验规则可能不一致
* Problem: RegisterPage had two password validation paths:
* 1. Visual validation provided by the PasswordChecklist component
* 2. A custom isStrongPassword function
* This could cause the validation rules to diverge.
*
* 修复:移除重复的 isStrongPassword 函数,统一使用 PasswordChecklist 的校验结果
* Fix: remove the duplicate isStrongPassword function and rely solely on the PasswordChecklist result.
*
* 本测试专注于验证密码校验逻辑的一致性,确保:
* 1. 移除了重复的 isStrongPassword 函数
* 2. 使用统一的 PasswordChecklist 校验
* 3. 特殊字符规则在正常显示和错误提示中保持一致
* This test focuses on verifying the consistency of the password validation logic, ensuring:
* 1. The duplicate isStrongPassword function is removed
* 2. A single PasswordChecklist validation is used
* 3. The special character rule stays consistent between normal display and error messages
*/
describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
/**
* 测试密码校验规则逻辑
* 这些测试验证密码校验的核心逻辑,与 PasswordChecklist 组件的规则一致
* Test the password validation rule logic
* These tests verify the core validation logic, consistent with the PasswordChecklist component rules
*/
describe('password validation rules', () => {
it('should validate minimum 8 characters', () => {
@@ -57,7 +57,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
it('should require special character from allowed set', () => {
// 根据 RegisterPage.tsx 中的设置,特殊字符正则为 /[@#$%!&*?]/
// Per the RegisterPage.tsx config, the special character regex is /[@#$%!&*?]/
const hasSpecialChar = (pwd: string) => /[@#$%!&*?]/.test(pwd)
expect(hasSpecialChar('NoSpecial123')).toBe(false)
@@ -70,7 +70,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
expect(hasSpecialChar('HasStar123*')).toBe(true)
expect(hasSpecialChar('HasQuestion123?')).toBe(true)
// 不在允许列表中的特殊字符应该不通过
// Special characters not in the allowed list should fail
expect(hasSpecialChar('HasCaret123^')).toBe(false)
expect(hasSpecialChar('HasTilde123~')).toBe(false)
})
@@ -86,8 +86,8 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
/**
* 测试完整的密码强度校验
* 模拟 PasswordChecklist 的完整校验逻辑
* Test the complete password strength validation
* Simulates the full PasswordChecklist validation logic
*/
describe('complete password strength validation', () => {
const validatePassword = (
@@ -193,22 +193,22 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
/**
* 测试特殊字符一致性
* 确保在 RegisterPage 的正常显示(第 229-251 行)和错误提示(第 300-323 行)中
* 使用相同的特殊字符正则 /[@#$%!&*?]/
* Test special character consistency
* Ensures the normal display (lines 229-251) and error messages (lines 300-323) in RegisterPage
* use the same special character regex /[@#$%!&*?]/
*/
describe('special character consistency', () => {
it('should use consistent special character regex across all validations', () => {
// RegisterPage 中两处 PasswordChecklist 都应该使用相同的 specialCharsRegex
// Both PasswordChecklist instances in RegisterPage should use the same specialCharsRegex
const specialCharsRegex = /[@#$%!&*?]/
// 测试允许的特殊字符
// Test the allowed special characters
const validSpecialChars = ['@', '#', '$', '%', '!', '&', '*', '?']
validSpecialChars.forEach((char) => {
expect(specialCharsRegex.test(char)).toBe(true)
})
// 测试不允许的特殊字符
// Test the disallowed special characters
const invalidSpecialChars = ['^', '~', '`', '(', ')', '-', '_', '=', '+']
invalidSpecialChars.forEach((char) => {
expect(specialCharsRegex.test(char)).toBe(false)
@@ -254,7 +254,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
/**
* 测试边界情况
* Test edge cases
*/
describe('edge cases', () => {
const validatePassword = (pwd: string, confirmPwd: string): boolean => {
@@ -311,16 +311,16 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
/**
* 测试重构后的一致性
* 确保移除 isStrongPassword 函数后,所有校验都通过 PasswordChecklist
* Test consistency after refactoring
* Ensures that after removing the isStrongPassword function, all validation goes through PasswordChecklist
*/
describe('refactoring consistency verification', () => {
it('should have removed duplicate isStrongPassword function', () => {
// 这个测试验证重构的意图:
// 在重构之前,存在一个 isStrongPassword 函数
// 重构后应该移除该函数,只使用 PasswordChecklist 的校验
// This test verifies the intent of the refactor:
// Before the refactor there was an isStrongPassword function
// After the refactor it should be removed, using only PasswordChecklist validation
// 我们通过模拟 PasswordChecklist 的逻辑来验证一致性
// We verify consistency by simulating the PasswordChecklist logic
const passwordChecklistValidation = (pwd: string, confirm: string) => {
return {
minLength: pwd.length >= 8,
@@ -332,7 +332,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
}
}
// 测试几个密码
// Test a few passwords
const testCases = [
{ pwd: 'Weak', confirm: 'Weak', shouldPass: false },
{ pwd: 'StrongPass123!', confirm: 'StrongPass123!', shouldPass: true },
@@ -351,7 +351,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
})
it('should use consistent validation logic across the component', () => {
// 验证校验逻辑的一致性
// Verify the consistency of the validation logic
const validation1 = {
minLength: 8,
requireCapital: true,
@@ -361,7 +361,7 @@ describe('RegisterPage - Password Validation Consistency (Logic Tests)', () => {
specialCharsRegex: /[@#$%!&*?]/,
}
// 在 RegisterPage 的正常显示和错误提示中应该使用相同的配置
// The normal display and error messages in RegisterPage should use the same config
const validation2 = {
minLength: 8,
requireCapital: true,

View File

@@ -57,7 +57,7 @@ export function RegisterPage() {
}
if (betaMode && !betaCode.trim()) {
setError('内测期间,注册需要提供内测码')
setError('A beta code is required to register during the closed beta')
return
}
@@ -123,9 +123,9 @@ export function RegisterPage() {
<div className="flex justify-between items-center mb-8">
<button
onClick={() => navigate('/')}
className="flex items-center gap-2 text-zinc-500 hover:text-white transition-colors group px-3 py-1.5 rounded border border-transparent hover:border-zinc-700 bg-black/20 backdrop-blur-sm"
className="flex items-center gap-2 text-nofx-text-muted hover:text-nofx-text transition-colors group px-3 py-1.5 rounded border border-transparent hover:border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper backdrop-blur-sm"
>
<div className="w-2 h-2 rounded-full bg-red-500 group-hover:animate-pulse"></div>
<div className="w-2 h-2 rounded-full bg-nofx-danger group-hover:animate-pulse"></div>
<span className="text-xs font-mono uppercase tracking-widest">
&lt; ABORT_REGISTRATION
</span>
@@ -135,7 +135,6 @@ export function RegisterPage() {
<div className="mb-8 text-center">
<div className="flex justify-center mb-6">
<div className="relative">
<div className="absolute -inset-2 bg-nofx-gold/20 rounded-full blur-xl animate-pulse"></div>
<img
src="/icons/nofx.svg"
alt="NoFx Logo"
@@ -143,56 +142,54 @@ export function RegisterPage() {
/>
</div>
</div>
<h1 className="text-3xl font-bold tracking-tighter text-white uppercase mb-2">
<h1 className="text-3xl font-bold tracking-tighter text-nofx-text uppercase mb-2">
<span className="text-nofx-gold">NEW_USER</span> ONBOARDING
</h1>
<p className="text-zinc-500 text-xs tracking-[0.2em] uppercase">
<p className="text-nofx-text-muted text-xs tracking-[0.2em] uppercase">
Initializing Registration Sequence...
</p>
</div>
<div className="bg-zinc-900/40 backdrop-blur-md border border-zinc-800 rounded-lg overflow-hidden shadow-2xl relative group">
<div className="absolute inset-0 bg-zinc-900/50 opacity-0 group-hover:opacity-100 transition duration-700 pointer-events-none"></div>
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900/80 border-b border-zinc-800">
<div className="bg-nofx-bg-lighter backdrop-blur-md border border-[rgba(26,24,19,0.14)] rounded-lg overflow-hidden shadow-lg relative group">
<div className="flex items-center justify-between px-4 py-2 bg-nofx-bg-deeper border-b border-[rgba(26,24,19,0.14)]">
<div className="flex gap-1.5">
<div
className="w-2.5 h-2.5 rounded-full bg-red-500/50 hover:bg-red-500 cursor-pointer transition-colors"
className="w-2.5 h-2.5 rounded-full bg-nofx-danger/50 hover:bg-nofx-danger cursor-pointer transition-colors"
onClick={() => navigate('/')}
title="Close / Return Home"
></div>
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500/50"></div>
<div className="w-2.5 h-2.5 rounded-full bg-green-500/50"></div>
<div className="w-2.5 h-2.5 rounded-full bg-nofx-gold/50"></div>
<div className="w-2.5 h-2.5 rounded-full bg-nofx-success/50"></div>
</div>
<div className="text-[10px] text-zinc-600 font-mono flex items-center gap-1">
<span className="text-emerald-500"></span> setup_account.sh
<div className="text-[10px] text-nofx-text-muted font-mono flex items-center gap-1">
<span className="text-nofx-success"></span> setup_account.sh
</div>
</div>
<div className="p-6 md:p-8 relative">
<div className="mb-6 font-mono text-xs space-y-1 text-zinc-500 border-b border-zinc-800/50 pb-4">
<div className="mb-6 font-mono text-xs space-y-1 text-nofx-text-muted border-b border-[rgba(26,24,19,0.14)] pb-4">
<div className="flex gap-2">
<span className="text-emerald-500"></span>
<span className="text-nofx-success"></span>
<span>
System Check: <span className="text-emerald-500">READY</span>
System Check: <span className="text-nofx-success">READY</span>
</span>
</div>
<div className="flex gap-2">
<span className="text-emerald-500"></span>
<span className="text-nofx-success"></span>
<span>Mode: {betaMode ? 'CLOSED_BETA CA1' : 'PUBLIC'}</span>
</div>
</div>
<form onSubmit={handleRegister} className="space-y-5">
<div>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
<label className="block text-xs uppercase tracking-wider text-nofx-text-muted mb-1.5 ml-1 font-bold">
{t('email', language)}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono"
className="w-full bg-nofx-bg border border-[rgba(26,24,19,0.14)] rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-nofx-text-muted text-nofx-text font-mono"
placeholder="user@nofx.os"
required
/>
@@ -200,7 +197,7 @@ export function RegisterPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
<label className="block text-xs uppercase tracking-wider text-nofx-text-muted mb-1.5 ml-1 font-bold">
{t('password', language)}
</label>
<div className="relative">
@@ -208,14 +205,14 @@ export function RegisterPage() {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10"
className="w-full bg-nofx-bg border border-[rgba(26,24,19,0.14)] rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-nofx-text-muted text-nofx-text font-mono pr-10"
placeholder="••••••••"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
@@ -223,7 +220,7 @@ export function RegisterPage() {
</div>
<div>
<label className="block text-xs uppercase tracking-wider text-zinc-500 mb-1.5 ml-1 font-bold">
<label className="block text-xs uppercase tracking-wider text-nofx-text-muted mb-1.5 ml-1 font-bold">
{t('confirmPassword', language)}
</label>
<div className="relative">
@@ -231,7 +228,7 @@ export function RegisterPage() {
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10"
className="w-full bg-nofx-bg border border-[rgba(26,24,19,0.14)] rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-nofx-text-muted text-nofx-text font-mono pr-10"
placeholder="••••••••"
required
/>
@@ -240,7 +237,7 @@ export function RegisterPage() {
onClick={() =>
setShowConfirmPassword(!showConfirmPassword)
}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
>
{showConfirmPassword ? (
<EyeOff size={16} />
@@ -252,12 +249,12 @@ export function RegisterPage() {
</div>
</div>
<div className="bg-zinc-900/50 p-3 rounded border border-zinc-800/50">
<div className="text-[10px] uppercase tracking-wider text-zinc-500 mb-2 font-bold flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-zinc-500"></div>
<div className="bg-nofx-bg-deeper p-3 rounded border border-[rgba(26,24,19,0.14)]">
<div className="text-[10px] uppercase tracking-wider text-nofx-text-muted mb-2 font-bold flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-nofx-text-muted"></div>
Password Strength Protocol
</div>
<div className="text-xs font-mono text-zinc-400">
<div className="text-xs font-mono text-nofx-text-muted">
<PasswordChecklist
rules={[
'minLength',
@@ -298,19 +295,19 @@ export function RegisterPage() {
e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase()
)
}
className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono tracking-widest"
className="w-full bg-nofx-bg border border-[rgba(26,24,19,0.14)] rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-nofx-text-muted text-nofx-text font-mono tracking-widest"
placeholder="XXXXXX"
maxLength={6}
required={betaMode}
/>
<p className="text-[10px] text-zinc-600 font-mono mt-1 ml-1">
<p className="text-[10px] text-nofx-text-muted font-mono mt-1 ml-1">
* CASE SENSITIVE ALPHANUMERIC
</p>
</div>
)}
{error && (
<div className="text-xs bg-red-500/10 border border-red-500/30 text-red-500 px-3 py-2 rounded font-mono">
<div className="text-xs bg-nofx-danger/10 border border-nofx-danger/30 text-nofx-danger px-3 py-2 rounded font-mono">
[REGISTRATION_ERROR]: {error}
</div>
)}
@@ -320,7 +317,7 @@ export function RegisterPage() {
disabled={
loading || (betaMode && !betaCode.trim()) || !passwordValid
}
className="w-full bg-nofx-gold text-black font-bold py-3 px-4 rounded text-sm tracking-wide uppercase hover:bg-yellow-400 transition-all transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed font-mono shadow-[0_0_15px_rgba(255,215,0,0.1)] hover:shadow-[0_0_25px_rgba(255,215,0,0.25)] flex items-center justify-center gap-2 group mt-4"
className="w-full bg-nofx-gold text-nofx-bg font-bold py-3 px-4 rounded text-sm tracking-wide uppercase hover:bg-nofx-gold-highlight transition-all transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed font-mono flex items-center justify-center gap-2 group mt-4"
>
{loading ? (
<span className="animate-pulse">INITIALIZING...</span>
@@ -336,25 +333,25 @@ export function RegisterPage() {
</form>
</div>
<div className="bg-zinc-900/50 p-3 flex justify-between items-center text-[10px] font-mono text-zinc-600 border-t border-zinc-800">
<div className="bg-nofx-bg-deeper p-3 flex justify-between items-center text-[10px] font-mono text-nofx-text-muted border-t border-[rgba(26,24,19,0.14)]">
<div>ENCRYPTION: AES-256</div>
<div>SECURE_REGISTRY</div>
</div>
</div>
<div className="text-center mt-8 space-y-4">
<p className="text-xs font-mono text-zinc-500">
<p className="text-xs font-mono text-nofx-text-muted">
EXISTING_OPERATOR?{' '}
<button
onClick={() => navigate('/login')}
className="text-nofx-gold hover:underline hover:text-yellow-300 transition-colors ml-1 uppercase"
className="text-nofx-gold hover:underline hover:text-nofx-gold-highlight transition-colors ml-1 uppercase"
>
ACCESS TERMINAL
</button>
</p>
<button
onClick={() => navigate('/')}
className="text-[10px] text-zinc-600 hover:text-red-500 transition-colors uppercase tracking-widest hover:underline decoration-red-500/30 font-mono"
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger transition-colors uppercase tracking-widest hover:underline decoration-nofx-danger/30 font-mono"
>
[ ABORT_REGISTRATION_RETURN_HOME ]
</button>

View File

@@ -89,15 +89,15 @@ describe('RegistrationDisabled Component', () => {
it('should have correct background color', () => {
const { container } = renderComponent()
const mainDiv = container.firstChild as HTMLElement
// Browser converts hex to rgb
expect(mainDiv.style.background).toMatch(/rgb\(11,\s*14,\s*17\)|#0B0E11/i)
// Browser converts hex to rgb (cream paper theme)
expect(mainDiv.style.background).toMatch(/rgb\(241,\s*236,\s*226\)|#F1ECE2/i)
})
it('should have correct text color', () => {
const { container } = renderComponent()
const mainDiv = container.firstChild as HTMLElement
// Browser converts hex to rgb
expect(mainDiv.style.color).toMatch(/rgb\(234,\s*236,\s*239\)|#EAECEF/i)
// Browser converts hex to rgb (ink text)
expect(mainDiv.style.color).toMatch(/rgb\(26,\s*24,\s*19\)|#1A1813/i)
})
it('should have centered layout', () => {

View File

@@ -13,7 +13,7 @@ export function RegistrationDisabled() {
return (
<div
className="min-h-screen flex items-center justify-center"
style={{ background: '#0B0E11', color: '#EAECEF' }}
style={{ background: '#F1ECE2', color: '#1A1813' }}
>
<div className="text-center max-w-md px-6">
<img
@@ -24,12 +24,12 @@ export function RegistrationDisabled() {
<h1 className="text-2xl font-semibold mb-3">
{t('registrationClosed', language)}
</h1>
<p className="text-sm text-gray-400">
<p className="text-sm text-nofx-text-muted">
{t('registrationClosedMessage', language)}
</p>
<button
className="mt-6 px-4 py-2 rounded text-sm font-semibold transition-colors hover:opacity-90"
style={{ background: '#F0B90B', color: '#000' }}
style={{ background: '#E0483B', color: '#F1ECE2' }}
onClick={handleBackToLogin}
>
{t('backToLogin', language)}

View File

@@ -25,7 +25,7 @@ export function ResetPasswordPage() {
}
return (
<div className="min-h-screen" style={{ background: '#0B0E11' }}>
<div className="min-h-screen" style={{ background: '#F1ECE2' }}>
<Header simple />
<div
@@ -36,8 +36,8 @@ export function ResetPasswordPage() {
{/* Back to Login */}
<button
onClick={() => navigate('/login')}
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
style={{ color: '#848E9C' }}
className="flex items-center gap-2 mb-6 text-sm hover:text-[#E0483B] transition-colors"
style={{ color: '#8A8478' }}
>
<ArrowLeft className="w-4 h-4" />
{t('backToLogin', language)}
@@ -47,11 +47,11 @@ export function ResetPasswordPage() {
<div className="text-center mb-8">
<div
className="w-16 h-16 mx-auto mb-4 flex items-center justify-center rounded-full"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
style={{ background: 'rgba(224, 72, 59, 0.1)' }}
>
<KeyRound className="w-8 h-8" style={{ color: '#F0B90B' }} />
<KeyRound className="w-8 h-8" style={{ color: '#E0483B' }} />
</div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
<h1 className="text-2xl font-bold" style={{ color: '#1A1813' }}>
{t('resetPasswordTitle', language)}
</h1>
</div>
@@ -59,22 +59,22 @@ export function ResetPasswordPage() {
{/* CLI recovery instructions */}
<div
className="rounded-lg p-6"
style={{ background: '#1E2329', border: '1px solid #2B3139' }}
style={{ background: '#F7F4EC', border: '1px solid rgba(26,24,19,0.14)' }}
>
<p
className="text-sm leading-relaxed mb-4"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{t('resetPasswordCliIntro', language)}
</p>
<div
className="flex items-center justify-between gap-3 rounded px-3 py-3 font-mono text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#E8E2D5', border: '1px solid rgba(26,24,19,0.14)' }}
>
<code
className="break-all"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
{RESET_PASSWORD_COMMAND}
</code>
@@ -82,11 +82,11 @@ export function ResetPasswordPage() {
type="button"
onClick={handleCopy}
className="shrink-0 btn-icon"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
aria-label={t('copy', language)}
>
{copied ? (
<Check className="w-4 h-4" style={{ color: '#0ECB81' }} />
<Check className="w-4 h-4" style={{ color: '#2E8B57' }} />
) : (
<Copy className="w-4 h-4" />
)}
@@ -95,7 +95,7 @@ export function ResetPasswordPage() {
<p
className="text-xs leading-relaxed mt-4"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('resetPasswordCliSecurityNote', language)}
</p>

View File

@@ -138,10 +138,10 @@ export function AdvancedChart({
// Indicator configuration
const [indicators, setIndicators] = useState<IndicatorConfig[]>([
{ id: 'volume', name: 'Volume', enabled: true, color: '#3B82F6' },
{ id: 'volume', name: 'Volume', enabled: true, color: '#E0483B' },
{ id: 'ma5', name: 'MA5', enabled: false, color: '#FF6B6B', params: { period: 5 } },
{ id: 'ma10', name: 'MA10', enabled: false, color: '#4ECDC4', params: { period: 10 } },
{ id: 'ma20', name: 'MA20', enabled: false, color: '#FFD93D', params: { period: 20 } },
{ id: 'ma20', name: 'MA20', enabled: false, color: '#E0483B', params: { period: 20 } },
{ id: 'ma60', name: 'MA60', enabled: false, color: '#95E1D3', params: { period: 60 } },
{ id: 'ema12', name: 'EMA12', enabled: false, color: '#A8E6CF', params: { period: 12 } },
{ id: 'ema26', name: 'EMA26', enabled: false, color: '#FFD3B6', params: { period: 26 } },
@@ -356,18 +356,18 @@ export function AdvancedChart({
width: chartContainerRef.current.clientWidth || 800,
height: chartContainerRef.current.clientHeight || height,
layout: {
background: { color: '#0B0E11' },
textColor: '#B7BDC6',
background: { color: '#F1ECE2' },
textColor: '#1A1813',
fontSize: 12,
},
grid: {
vertLines: {
color: 'rgba(43, 49, 57, 0.2)',
color: 'rgba(26, 24, 19, 0.08)',
style: 1,
visible: true,
},
horzLines: {
color: 'rgba(43, 49, 57, 0.2)',
color: 'rgba(26, 24, 19, 0.08)',
style: 1,
visible: true,
},
@@ -375,20 +375,20 @@ export function AdvancedChart({
crosshair: {
mode: 1,
vertLine: {
color: 'rgba(240, 185, 11, 0.5)',
color: 'rgba(224, 72, 59, 0.5)',
width: 1,
style: 2,
labelBackgroundColor: '#F0B90B',
labelBackgroundColor: '#E0483B',
},
horzLine: {
color: 'rgba(240, 185, 11, 0.5)',
color: 'rgba(224, 72, 59, 0.5)',
width: 1,
style: 2,
labelBackgroundColor: '#F0B90B',
labelBackgroundColor: '#E0483B',
},
},
rightPriceScale: {
borderColor: '#2B3139',
borderColor: 'rgba(26, 24, 19, 0.14)',
scaleMargins: {
top: 0.1,
bottom: 0.25,
@@ -397,7 +397,7 @@ export function AdvancedChart({
entireTextOnly: false,
},
timeScale: {
borderColor: '#2B3139',
borderColor: 'rgba(26, 24, 19, 0.14)',
timeVisible: true,
secondsVisible: false,
borderVisible: true,
@@ -433,18 +433,18 @@ export function AdvancedChart({
// Create candlestick series
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#0ECB81',
downColor: '#F6465D',
borderUpColor: '#0ECB81',
borderDownColor: '#F6465D',
wickUpColor: '#0ECB81',
wickDownColor: '#F6465D',
upColor: '#2E8B57',
downColor: '#D6433A',
borderUpColor: '#2E8B57',
borderDownColor: '#D6433A',
wickUpColor: '#2E8B57',
wickDownColor: '#D6433A',
})
candlestickSeriesRef.current = candlestickSeries as any
// Create volume series
const volumeSeries = chart.addSeries(HistogramSeries, {
color: '#26a69a',
color: '#2E8B57',
priceFormat: {
type: 'volume',
},
@@ -579,7 +579,7 @@ export function AdvancedChart({
const volumeData = klineData.map((k: Kline) => ({
time: k.time,
value: k.volume || 0,
color: k.close >= k.open ? 'rgba(14, 203, 129, 0.5)' : 'rgba(246, 70, 93, 0.5)',
color: k.close >= k.open ? 'rgba(46, 139, 87, 0.5)' : 'rgba(214, 67, 58, 0.5)',
}))
volumeSeriesRef.current.setData(volumeData)
} else {
@@ -666,7 +666,7 @@ export function AdvancedChart({
markers.push({
time: candleTime as Time,
position: 'belowBar' as const,
color: '#0ECB81',
color: '#2E8B57',
shape: 'circle' as const,
text: counts.buys > 1 ? `B${counts.buys}` : 'B',
size: 1,
@@ -677,7 +677,7 @@ export function AdvancedChart({
markers.push({
time: candleTime as Time,
position: 'aboveBar' as const,
color: '#F6465D',
color: '#D6433A',
shape: 'circle' as const,
text: counts.sells > 1 ? `S${counts.sells}` : 'S',
size: 1,
@@ -780,18 +780,18 @@ export function AdvancedChart({
const isLimit = order.type === 'LIMIT'
// Set price line style
let lineColor = '#F0B90B' // Default yellow
let lineColor = '#E0483B' // Default vermilion
const lineStyle = 2 // dashed
let title = ''
if (isStopLoss) {
lineColor = '#F6465D' // red - stop loss
lineColor = '#D6433A' // red - stop loss
title = `SL ${order.quantity}`
} else if (isTakeProfit) {
lineColor = '#0ECB81' // green - take profit
lineColor = '#2E8B57' // green - take profit
title = `TP ${order.quantity}`
} else if (isLimit) {
lineColor = '#F0B90B' // yellow - limit order
lineColor = '#E0483B' // vermilion - limit order
title = `Limit ${order.side} ${order.quantity}`
} else {
title = `${order.type} ${order.quantity}`
@@ -918,10 +918,10 @@ export function AdvancedChart({
<div
className="relative shadow-xl"
style={{
background: 'linear-gradient(180deg, #0F1215 0%, #0B0E11 100%)',
background: '#F1ECE2',
borderRadius: '12px',
overflow: 'hidden',
border: '1px solid rgba(43, 49, 57, 0.5)',
border: '1px solid rgba(26, 24, 19, 0.14)',
height: '100%',
display: 'flex',
flexDirection: 'column',
@@ -930,19 +930,19 @@ export function AdvancedChart({
{/* Compact Professional Header */}
<div
className="flex items-center justify-between px-4 py-2"
style={{ borderBottom: '1px solid rgba(43, 49, 57, 0.6)', background: '#0D1117', flexShrink: 0 }}
style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)', background: '#F7F4EC', flexShrink: 0 }}
>
{/* Left: Symbol Info + Price */}
<div className="flex items-center gap-4">
{/* Symbol & Interval */}
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-white">{symbol}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[#1F2937] text-gray-400">{interval}</span>
<span className="text-sm font-bold text-nofx-text">{symbol}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-deeper text-nofx-text-muted">{interval}</span>
<span
className="text-[10px] px-1.5 py-0.5 rounded font-medium uppercase"
style={{
background: exchange === 'hyperliquid' ? 'rgba(80, 227, 194, 0.1)' : 'rgba(243, 186, 47, 0.1)',
color: exchange === 'hyperliquid' ? '#50E3C2' : '#F3BA2F',
background: 'rgba(224, 72, 59, 0.1)',
color: '#E0483B',
}}
>
{exchange?.toUpperCase()}
@@ -951,10 +951,10 @@ export function AdvancedChart({
{/* Price Display */}
{marketStats && (
<div className="flex items-center gap-3 pl-3 border-l border-[#2B3139]">
<div className="flex items-center gap-3 pl-3 border-l border-[rgba(26,24,19,0.14)]">
<span
className="text-base font-bold tabular-nums"
style={{ color: marketStats.priceChange >= 0 ? '#10B981' : '#EF4444' }}
style={{ color: marketStats.priceChange >= 0 ? '#2E8B57' : '#D6433A' }}
>
{marketStats.price.toLocaleString(undefined, {
minimumFractionDigits: 2,
@@ -964,19 +964,19 @@ export function AdvancedChart({
<span
className="text-xs font-medium px-1.5 py-0.5 rounded tabular-nums"
style={{
background: marketStats.priceChange >= 0 ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
color: marketStats.priceChange >= 0 ? '#10B981' : '#EF4444',
background: marketStats.priceChange >= 0 ? 'rgba(46, 139, 87, 0.1)' : 'rgba(214, 67, 58, 0.1)',
color: marketStats.priceChange >= 0 ? '#2E8B57' : '#D6433A',
}}
>
{marketStats.priceChange >= 0 ? '+' : ''}{marketStats.priceChangePercent.toFixed(2)}%
</span>
{/* Compact H/L */}
<div className="flex items-center gap-2 text-[11px] text-gray-500">
<span>H <span className="text-gray-300">{marketStats.high.toFixed(2)}</span></span>
<span>L <span className="text-gray-300">{marketStats.low.toFixed(2)}</span></span>
<div className="flex items-center gap-2 text-[11px] text-nofx-text-muted">
<span>H <span className="text-nofx-text">{marketStats.high.toFixed(2)}</span></span>
<span>L <span className="text-nofx-text">{marketStats.low.toFixed(2)}</span></span>
{marketStats.volume > 0 && baseUnit && (
<span>Vol <span className="text-gray-300">{formatVolume(marketStats.volume)}</span></span>
<span>Vol <span className="text-nofx-text">{formatVolume(marketStats.volume)}</span></span>
)}
</div>
</div>
@@ -986,7 +986,7 @@ export function AdvancedChart({
{/* Right: Controls */}
<div className="flex items-center gap-1.5">
{loading && (
<span className="text-[10px] text-yellow-400 animate-pulse mr-2">
<span className="text-[10px] text-nofx-gold animate-pulse mr-2">
{t('advancedChart.updating', language)}
</span>
)}
@@ -994,8 +994,8 @@ export function AdvancedChart({
onClick={() => setShowIndicatorPanel(!showIndicatorPanel)}
className="flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium transition-all"
style={{
background: showIndicatorPanel ? 'rgba(96, 165, 250, 0.15)' : 'transparent',
color: showIndicatorPanel ? '#60A5FA' : '#6B7280',
background: showIndicatorPanel ? 'rgba(224, 72, 59, 0.12)' : 'transparent',
color: showIndicatorPanel ? '#E0483B' : '#8A8478',
}}
>
<Settings className="w-3 h-3" />
@@ -1006,8 +1006,8 @@ export function AdvancedChart({
onClick={() => setShowOrderMarkers(!showOrderMarkers)}
className="flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium transition-all"
style={{
background: showOrderMarkers ? 'rgba(16, 185, 129, 0.15)' : 'transparent',
color: showOrderMarkers ? '#10B981' : '#6B7280',
background: showOrderMarkers ? 'rgba(46, 139, 87, 0.15)' : 'transparent',
color: showOrderMarkers ? '#2E8B57' : '#8A8478',
}}
title={t('advancedChart.orderMarkers', language)}
>
@@ -1021,8 +1021,8 @@ export function AdvancedChart({
<div
className="absolute top-16 right-4 z-10 rounded-lg shadow-2xl backdrop-blur-sm"
style={{
background: 'linear-gradient(135deg, #1A1E23 0%, #0F1215 100%)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: '#F7F4EC',
border: '1px solid rgba(224, 72, 59, 0.2)',
maxHeight: '500px',
minWidth: '280px',
overflowY: 'auto',
@@ -1031,17 +1031,17 @@ export function AdvancedChart({
{/* Title bar */}
<div
className="flex items-center justify-between px-4 py-3 border-b"
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
style={{ borderColor: 'rgba(26, 24, 19, 0.14)' }}
>
<div className="flex items-center gap-2">
<BarChart2 className="w-4 h-4 text-yellow-400" />
<h4 className="text-sm font-bold text-white">
<BarChart2 className="w-4 h-4 text-nofx-gold" />
<h4 className="text-sm font-bold text-nofx-text">
{t('advancedChart.technicalIndicators', language)}
</h4>
</div>
<button
onClick={() => setShowIndicatorPanel(false)}
className="text-gray-400 hover:text-white transition-colors"
className="text-nofx-text-muted hover:text-nofx-text transition-colors"
>
<span className="text-lg">×</span>
</button>
@@ -1052,25 +1052,25 @@ export function AdvancedChart({
{indicators.map(indicator => (
<label
key={indicator.id}
className="flex items-center gap-3 p-2.5 rounded-md hover:bg-white/5 cursor-pointer transition-all group"
className="flex items-center gap-3 p-2.5 rounded-md hover:bg-black/5 cursor-pointer transition-all group"
>
<div className="relative">
<input
type="checkbox"
checked={indicator.enabled}
onChange={() => toggleIndicator(indicator.id)}
className="w-4 h-4 rounded border-gray-600 text-yellow-500 focus:ring-2 focus:ring-yellow-500/50"
className="w-4 h-4 rounded border-[rgba(26,24,19,0.3)] text-nofx-gold focus:ring-2 focus:ring-nofx-gold/50"
/>
</div>
<div
className="w-8 h-3 rounded-sm border border-white/10"
className="w-8 h-3 rounded-sm border border-[rgba(26,24,19,0.14)]"
style={{ backgroundColor: indicator.color }}
></div>
<span className="text-sm text-gray-300 group-hover:text-white transition-colors flex-1">
<span className="text-sm text-nofx-text-muted group-hover:text-nofx-text transition-colors flex-1">
{indicator.name}
</span>
{indicator.enabled && (
<span className="text-xs text-yellow-400"></span>
<span className="text-xs text-nofx-gold"></span>
)}
</label>
))}
@@ -1078,8 +1078,8 @@ export function AdvancedChart({
{/* Bottom hint */}
<div
className="px-4 py-2 text-xs text-gray-500 border-t"
style={{ borderColor: 'rgba(43, 49, 57, 0.5)' }}
className="px-4 py-2 text-xs text-nofx-text-muted border-t"
style={{ borderColor: 'rgba(26, 24, 19, 0.14)' }}
>
{t('advancedChart.clickToToggle', language)}
</div>
@@ -1099,19 +1099,19 @@ export function AdvancedChart({
left: '10px',
top: '10px',
padding: '8px 12px',
background: 'rgba(15, 18, 21, 0.95)',
border: '1px solid rgba(240, 185, 11, 0.3)',
background: 'rgba(247, 244, 236, 0.95)',
border: '1px solid rgba(224, 72, 59, 0.3)',
borderRadius: '6px',
color: '#EAECEF',
color: '#1A1813',
fontSize: '12px',
fontFamily: 'monospace',
pointerEvents: 'none',
zIndex: 10,
backdropFilter: 'blur(10px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)',
boxShadow: '0 4px 12px rgba(26, 24, 19, 0.15)',
}}
>
<div style={{ marginBottom: '6px', color: '#F0B90B', fontWeight: 'bold', fontSize: '11px' }}>
<div style={{ marginBottom: '6px', color: '#E0483B', fontWeight: 'bold', fontSize: '11px' }}>
{new Date((tooltipData.time as number) * 1000).toLocaleString(language === 'zh' ? 'zh-CN' : 'en-US', {
month: 'short',
day: 'numeric',
@@ -1120,18 +1120,18 @@ export function AdvancedChart({
})}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '4px 12px', fontSize: '11px' }}>
<span style={{ color: '#848E9C' }}>O:</span>
<span style={{ color: '#EAECEF', fontWeight: '500' }}>{tooltipData.open?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>O:</span>
<span style={{ color: '#1A1813', fontWeight: '500' }}>{tooltipData.open?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>H:</span>
<span style={{ color: '#0ECB81', fontWeight: '500' }}>{tooltipData.high?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>H:</span>
<span style={{ color: '#2E8B57', fontWeight: '500' }}>{tooltipData.high?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>L:</span>
<span style={{ color: '#F6465D', fontWeight: '500' }}>{tooltipData.low?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>L:</span>
<span style={{ color: '#D6433A', fontWeight: '500' }}>{tooltipData.low?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>C:</span>
<span style={{ color: '#8A8478' }}>C:</span>
<span style={{
color: tooltipData.close >= tooltipData.open ? '#0ECB81' : '#F6465D',
color: tooltipData.close >= tooltipData.open ? '#2E8B57' : '#D6433A',
fontWeight: 'bold'
}}>
{tooltipData.close?.toFixed(2)}
@@ -1139,8 +1139,8 @@ export function AdvancedChart({
{tooltipData.volume > 0 && baseUnit && (
<>
<span style={{ color: '#848E9C' }}>V({baseUnit}):</span>
<span style={{ color: '#3B82F6', fontWeight: '500' }}>
<span style={{ color: '#8A8478' }}>V({baseUnit}):</span>
<span style={{ color: '#E0483B', fontWeight: '500' }}>
{formatVolume(tooltipData.volume)}
</span>
</>
@@ -1148,8 +1148,8 @@ export function AdvancedChart({
{tooltipData.quoteVolume > 0 && quoteUnit && (
<>
<span style={{ color: '#848E9C' }}>V({quoteUnit}):</span>
<span style={{ color: '#3B82F6', fontWeight: '500' }}>
<span style={{ color: '#8A8478' }}>V({quoteUnit}):</span>
<span style={{ color: '#E0483B', fontWeight: '500' }}>
{formatVolume(tooltipData.quoteVolume)}
</span>
</>
@@ -1173,10 +1173,9 @@ export function AdvancedChart({
style={{
fontSize: '56px',
fontWeight: '700',
color: 'rgba(240, 185, 11, 0.12)',
color: 'rgba(224, 72, 59, 0.12)',
letterSpacing: '4px',
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
textShadow: '0 2px 30px rgba(240, 185, 11, 0.2)',
}}
>
NOFX
@@ -1188,11 +1187,11 @@ export function AdvancedChart({
{error && (
<div
className="absolute inset-0 flex items-center justify-center"
style={{ background: 'rgba(11, 14, 17, 0.9)' }}
style={{ background: 'rgba(241, 236, 226, 0.9)' }}
>
<div className="text-center">
<div className="text-2xl mb-2"></div>
<div style={{ color: '#F6465D' }}>{error}</div>
<div style={{ color: '#D6433A' }}>{error}</div>
</div>
</div>
)}

View File

@@ -148,7 +148,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
}
return (
<div className={`nofx-glass rounded-lg border border-white/5 relative z-10 w-full flex flex-col transition-all duration-300 ${typeof window !== 'undefined' && window.innerWidth < 768 ? 'h-[500px]' : 'h-[600px]'
<div className={`nofx-glass rounded-lg border border-[rgba(26,24,19,0.1)] relative z-10 w-full flex flex-col transition-all duration-300 ${typeof window !== 'undefined' && window.innerWidth < 768 ? 'h-[500px]' : 'h-[600px]'
}`}>
{/*
Premium Professional Toolbar
@@ -156,16 +156,16 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
Desktop: Standard flex-wrap/nowrap
*/}
<div
className="relative z-20 flex flex-wrap md:flex-nowrap items-center justify-between gap-y-2 px-3 py-2 shrink-0 backdrop-blur-md bg-[#0B0E11]/80 rounded-t-lg"
style={{ borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}
className="relative z-20 flex flex-wrap md:flex-nowrap items-center justify-between gap-y-2 px-3 py-2 shrink-0 backdrop-blur-md bg-nofx-bg/80 rounded-t-lg"
style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}
>
{/* Left: Tab Switcher */}
<div className="flex flex-wrap items-center gap-1">
<button
onClick={() => setActiveTab('equity')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[11px] font-medium transition-all ${activeTab === 'equity'
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_10px_rgba(240,185,11,0.1)]'
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-white/5'
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20'
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-black/5'
}`}
>
<BarChart3 className="w-3.5 h-3.5" />
@@ -176,8 +176,8 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
<button
onClick={() => setActiveTab('kline')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[11px] font-medium transition-all ${activeTab === 'kline'
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_10px_rgba(240,185,11,0.1)]'
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-white/5'
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20'
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-black/5'
}`}
>
<CandlestickChart className="w-3.5 h-3.5" />
@@ -187,7 +187,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
{/* Market Type Pills - Only when kline active, HIDDEN on mobile to save space */}
{activeTab === 'kline' && (
<div className="hidden md:flex items-center gap-1 ml-2 border-l border-white/10 pl-2">
<div className="hidden md:flex items-center gap-1 ml-2 border-l border-[rgba(26,24,19,0.14)] pl-2">
{(Object.keys(MARKET_CONFIG) as MarketType[]).map((type) => {
const config = MARKET_CONFIG[type]
const isActive = marketType === type
@@ -196,8 +196,8 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
key={type}
onClick={() => handleMarketTypeChange(type)}
className={`px-2.5 py-1 text-[10px] font-medium rounded transition-all border ${isActive
? 'bg-white/10 text-white border-white/20'
: 'text-nofx-text-muted border-transparent hover:text-nofx-text-main hover:bg-white/5'
? 'bg-nofx-gold/10 text-nofx-gold border-nofx-gold/20'
: 'text-nofx-text-muted border-transparent hover:text-nofx-text-main hover:bg-black/5'
}`}
>
<span className="mr-1 opacity-70">{config.icon}</span>
@@ -218,22 +218,22 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
<>
<button
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center gap-1.5 px-2.5 py-1 bg-black/40 border border-white/10 rounded text-[11px] font-bold text-nofx-text-main hover:border-nofx-gold/30 hover:text-nofx-gold transition-all"
className="flex items-center gap-1.5 px-2.5 py-1 bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded text-[11px] font-bold text-nofx-text-main hover:border-nofx-gold/30 hover:text-nofx-gold transition-all"
>
<span>{chartSymbolDisplay}</span>
<ChevronDown className={`w-3 h-3 text-nofx-text-muted transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
</button>
{showDropdown && (
<div className="absolute top-full right-0 mt-2 w-64 bg-[#0B0E11] border border-white/10 rounded-lg shadow-[0_10px_40px_-10px_rgba(0,0,0,0.5)] z-50 overflow-hidden nofx-glass ring-1 ring-white/5">
<div className="p-2 border-b border-white/5">
<div className="flex items-center gap-2 px-2 py-1.5 bg-black/40 rounded border border-white/10 focus-within:border-nofx-gold/50 transition-colors">
<div className="absolute top-full right-0 mt-2 w-64 bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] rounded-lg shadow-[0_10px_40px_-10px_rgba(26,24,19,0.2)] z-50 overflow-hidden nofx-glass ring-1 ring-[rgba(26,24,19,0.08)]">
<div className="p-2 border-b border-[rgba(26,24,19,0.1)]">
<div className="flex items-center gap-2 px-2 py-1.5 bg-nofx-bg-deeper rounded border border-[rgba(26,24,19,0.14)] focus-within:border-nofx-gold/50 transition-colors">
<Search className="w-3.5 h-3.5 text-nofx-text-muted" />
<input
type="text"
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
placeholder="Search symbol..."
className="flex-1 bg-transparent text-[11px] text-white placeholder-gray-600 focus:outline-none font-mono"
className="flex-1 bg-transparent text-[11px] text-nofx-text placeholder-nofx-text-muted focus:outline-none font-mono"
autoFocus
/>
</div>
@@ -245,12 +245,12 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
const labels: Record<string, string> = { crypto: 'Crypto', stock: 'Stocks', forex: 'Forex', commodity: 'Commodities', index: 'Indices', pre_ipo: 'Pre-IPO' }
return (
<div key={category}>
<div className="px-3 py-1.5 text-[9px] font-bold text-nofx-text-muted/60 bg-white/5 uppercase tracking-wider">{labels[category]}</div>
<div className="px-3 py-1.5 text-[9px] font-bold text-nofx-text-muted/60 bg-black/5 uppercase tracking-wider">{labels[category]}</div>
{categorySymbols.map(s => (
<button
key={s.symbol}
onClick={() => { setChartSymbol(s.symbol); setShowDropdown(false); setSearchFilter('') }}
className={`w-full px-3 py-2 text-left text-[11px] font-mono hover:bg-white/5 transition-all flex items-center justify-between ${chartSymbol === s.symbol ? 'bg-nofx-gold/10 text-nofx-gold' : 'text-nofx-text-muted'}`}
className={`w-full px-3 py-2 text-left text-[11px] font-mono hover:bg-black/5 transition-all flex items-center justify-between ${chartSymbol === s.symbol ? 'bg-nofx-gold/10 text-nofx-gold' : 'text-nofx-text-muted'}`}
>
<span>{s.display || s.symbol}</span>
<span className="text-[9px] opacity-40">{s.name}</span>
@@ -264,19 +264,19 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
)}
</>
) : (
<span className="px-2.5 py-1 bg-black/40 border border-white/10 rounded text-[11px] font-bold text-nofx-text-main font-mono">{chartSymbol}</span>
<span className="px-2.5 py-1 bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded text-[11px] font-bold text-nofx-text-main font-mono">{chartSymbol}</span>
)}
</div>
{/* Interval Selector - Allow scrolling if needed */}
<div className="flex items-center bg-black/40 rounded border border-white/10 overflow-x-auto no-scrollbar max-w-[200px] md:max-w-none">
<div className="flex items-center bg-nofx-bg-deeper rounded border border-[rgba(26,24,19,0.14)] overflow-x-auto no-scrollbar max-w-[200px] md:max-w-none">
{INTERVALS.map((int) => (
<button
key={int.value}
onClick={() => setInterval(int.value)}
className={`px-2 py-1 text-[10px] font-medium transition-all ${interval === int.value
? 'bg-nofx-gold/20 text-nofx-gold'
: 'text-nofx-text-muted hover:text-white hover:bg-white/5'
: 'text-nofx-text-muted hover:text-nofx-text hover:bg-black/5'
}`}
>
{int.label}
@@ -291,9 +291,9 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
value={symbolInput}
onChange={(e) => setSymbolInput(e.target.value)}
placeholder="Sym"
className="w-16 px-2 py-1 bg-black/40 border border-white/10 rounded-l text-[10px] text-white placeholder-gray-600 focus:outline-none focus:border-nofx-gold/50 font-mono transition-colors"
className="w-16 px-2 py-1 bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-l text-[10px] text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/50 font-mono transition-colors"
/>
<button type="submit" className="px-2 py-1 bg-white/5 border border-white/10 border-l-0 rounded-r text-[10px] text-nofx-text-muted hover:text-white hover:bg-white/10 transition-all">
<button type="submit" className="px-2 py-1 bg-black/5 border border-[rgba(26,24,19,0.14)] border-l-0 rounded-r text-[10px] text-nofx-text-muted hover:text-nofx-text hover:bg-black/10 transition-all">
Go
</button>
</form>
@@ -302,7 +302,7 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
</div>
{/* Tab Content - Chart autosizes to this container */}
<div className="relative flex-1 bg-[#0B0E11]/50 rounded-b-lg overflow-hidden h-full min-h-0">
<div className="relative flex-1 bg-nofx-bg/50 rounded-b-lg overflow-hidden h-full min-h-0">
<AnimatePresence mode="wait">
{activeTab === 'equity' ? (
<motion.div

View File

@@ -210,21 +210,21 @@ export function ChartWithOrders({
width: chartContainerRef.current.clientWidth,
height: height,
layout: {
background: { color: '#0B0E11' },
textColor: '#EAECEF',
background: { color: '#F1ECE2' },
textColor: '#1A1813',
},
grid: {
vertLines: { color: 'rgba(43, 49, 57, 0.5)' },
horzLines: { color: 'rgba(43, 49, 57, 0.5)' },
vertLines: { color: 'rgba(26, 24, 19, 0.08)' },
horzLines: { color: 'rgba(26, 24, 19, 0.08)' },
},
crosshair: {
mode: 1, // Normal crosshair
},
rightPriceScale: {
borderColor: '#2B3139',
borderColor: 'rgba(26, 24, 19, 0.14)',
},
timeScale: {
borderColor: '#2B3139',
borderColor: 'rgba(26, 24, 19, 0.14)',
timeVisible: true,
secondsVisible: false,
},
@@ -246,12 +246,12 @@ export function ChartWithOrders({
// Create candlestick series (using v5 API)
const candlestickSeries = chart.addSeries(CandlestickSeries, {
upColor: '#0ECB81',
downColor: '#F6465D',
borderUpColor: '#0ECB81',
borderDownColor: '#F6465D',
wickUpColor: '#0ECB81',
wickDownColor: '#F6465D',
upColor: '#2E8B57',
downColor: '#D6433A',
borderUpColor: '#2E8B57',
borderDownColor: '#D6433A',
wickUpColor: '#2E8B57',
wickDownColor: '#D6433A',
})
candlestickSeriesRef.current = candlestickSeries as any
@@ -380,7 +380,7 @@ export function ChartWithOrders({
markers.push({
time: alignedTime as Time,
position: 'belowBar' as const,
color: isBuy ? '#0ECB81' : '#F6465D',
color: isBuy ? '#2E8B57' : '#D6433A',
shape: 'circle' as const,
text: isBuy ? 'B' : 'S',
price: order.price,
@@ -431,17 +431,17 @@ export function ChartWithOrders({
}, [symbol, interval, traderID, language])
return (
<div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden' }}>
<div className="relative" style={{ background: '#F1ECE2', borderRadius: '8px', overflow: 'hidden' }}>
{/* Title bar */}
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}>
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="flex items-center gap-3">
<span className="text-xl">📈</span>
<h3 className="text-lg font-bold" style={{ color: '#EAECEF' }}>
<h3 className="text-lg font-bold" style={{ color: '#1A1813' }}>
{symbol} {interval}
</h3>
</div>
{loading && (
<div className="text-sm" style={{ color: '#848E9C' }}>
<div className="text-sm" style={{ color: '#8A8478' }}>
{t('chartWithOrders.loading', language)}
</div>
)}
@@ -460,19 +460,19 @@ export function ChartWithOrders({
left: '10px',
top: '10px',
padding: '8px 12px',
background: 'rgba(15, 18, 21, 0.95)',
border: '1px solid rgba(240, 185, 11, 0.3)',
background: 'rgba(247, 244, 236, 0.95)',
border: '1px solid rgba(224, 72, 59, 0.3)',
borderRadius: '6px',
color: '#EAECEF',
color: '#1A1813',
fontSize: '12px',
fontFamily: 'monospace',
pointerEvents: 'none',
zIndex: 10,
backdropFilter: 'blur(10px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)',
boxShadow: '0 4px 12px rgba(26, 24, 19, 0.15)',
}}
>
<div style={{ marginBottom: '6px', color: '#F0B90B', fontWeight: 'bold', fontSize: '11px' }}>
<div style={{ marginBottom: '6px', color: '#E0483B', fontWeight: 'bold', fontSize: '11px' }}>
{new Date((tooltipData.time as number) * 1000).toLocaleString(language === 'zh' ? 'zh-CN' : 'en-US', {
month: 'short',
day: 'numeric',
@@ -481,18 +481,18 @@ export function ChartWithOrders({
})}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '4px 12px', fontSize: '11px' }}>
<span style={{ color: '#848E9C' }}>O:</span>
<span style={{ color: '#EAECEF', fontWeight: '500' }}>{tooltipData.open?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>O:</span>
<span style={{ color: '#1A1813', fontWeight: '500' }}>{tooltipData.open?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>H:</span>
<span style={{ color: '#0ECB81', fontWeight: '500' }}>{tooltipData.high?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>H:</span>
<span style={{ color: '#2E8B57', fontWeight: '500' }}>{tooltipData.high?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>L:</span>
<span style={{ color: '#F6465D', fontWeight: '500' }}>{tooltipData.low?.toFixed(2)}</span>
<span style={{ color: '#8A8478' }}>L:</span>
<span style={{ color: '#D6433A', fontWeight: '500' }}>{tooltipData.low?.toFixed(2)}</span>
<span style={{ color: '#848E9C' }}>C:</span>
<span style={{ color: '#8A8478' }}>C:</span>
<span style={{
color: tooltipData.close >= tooltipData.open ? '#0ECB81' : '#F6465D',
color: tooltipData.close >= tooltipData.open ? '#2E8B57' : '#D6433A',
fontWeight: 'bold'
}}>
{tooltipData.close?.toFixed(2)}
@@ -506,23 +506,23 @@ export function ChartWithOrders({
{error && (
<div
className="absolute inset-0 flex items-center justify-center"
style={{ background: 'rgba(11, 14, 17, 0.9)' }}
style={{ background: 'rgba(241, 236, 226, 0.9)' }}
>
<div className="text-center">
<div className="text-2xl mb-2"></div>
<div style={{ color: '#F6465D' }}>{error}</div>
<div style={{ color: '#D6433A' }}>{error}</div>
</div>
</div>
)}
{/* Legend */}
<div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}>
<div className="flex items-center gap-4 p-4 text-xs" style={{ borderTop: '1px solid rgba(26, 24, 19, 0.14)', color: '#8A8478' }}>
<div className="flex items-center gap-2">
<span className="font-bold" style={{ color: '#0ECB81' }}>B</span>
<span className="font-bold" style={{ color: '#2E8B57' }}>B</span>
<span>{t('chartWithOrders.buy', language)}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-bold" style={{ color: '#F6465D' }}>S</span>
<span className="font-bold" style={{ color: '#D6433A' }}>S</span>
<span>{t('chartWithOrders.sell', language)}</span>
</div>
</div>

View File

@@ -26,7 +26,7 @@ export function ChartWithOrdersSimple({
setError(null)
try {
// 从我们自己的服务获取K线数据
// Fetch kline data from our own service
const limit = 100
const klineUrl = `/api/klines?symbol=${symbol}&interval=${interval}&limit=${limit}`
@@ -40,7 +40,7 @@ export function ChartWithOrdersSimple({
console.log('[ChartSimple] Received klines:', klineResult.data.length)
setKlineCount(klineResult.data.length)
// 测试获取订单数据
// Test fetching order data
if (traderID) {
const tradesUrl = `/api/trades?trader_id=${traderID}&symbol=${symbol}&limit=100`
console.log('[ChartSimple] Fetching trades from:', tradesUrl)
@@ -66,51 +66,51 @@ export function ChartWithOrdersSimple({
}, [symbol, interval, traderID])
return (
<div className="relative" style={{ background: '#0B0E11', borderRadius: '8px', overflow: 'hidden', minHeight: height }}>
{/* 标题栏 */}
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid #2B3139' }}>
<div className="relative" style={{ background: '#F1ECE2', borderRadius: '8px', overflow: 'hidden', minHeight: height }}>
{/* Title bar */}
<div className="flex items-center justify-between p-4" style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="flex items-center gap-3">
<span className="text-xl">📈</span>
<h3 className="text-lg font-bold" style={{ color: '#EAECEF' }}>
{symbol} {interval} ()
<h3 className="text-lg font-bold" style={{ color: '#1A1813' }}>
{symbol} {interval} (Test Mode)
</h3>
</div>
{loading && (
<div className="text-sm" style={{ color: '#848E9C' }}>
...
<div className="text-sm" style={{ color: '#8A8478' }}>
Loading...
</div>
)}
</div>
{/* 测试信息 */}
{/* Test info */}
<div className="p-8 space-y-4">
{error ? (
<div className="text-center">
<div className="text-2xl mb-2"></div>
<div style={{ color: '#F6465D' }}>{error}</div>
<div style={{ color: '#D6433A' }}>{error}</div>
</div>
) : (
<>
<div className="p-4 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="text-sm mb-2" style={{ color: '#848E9C' }}>K线数据</div>
<div className="text-2xl font-bold" style={{ color: '#0ECB81' }}>
{klineCount} K线
<div className="p-4 rounded" style={{ background: '#F7F4EC', border: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="text-sm mb-2" style={{ color: '#8A8478' }}>Binance Kline Data</div>
<div className="text-2xl font-bold" style={{ color: '#2E8B57' }}>
{klineCount} klines
</div>
</div>
{traderID && (
<div className="p-4 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="text-sm mb-2" style={{ color: '#848E9C' }}></div>
<div className="text-2xl font-bold" style={{ color: '#F0B90B' }}>
{orderCount}
<div className="p-4 rounded" style={{ background: '#F7F4EC', border: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="text-sm mb-2" style={{ color: '#8A8478' }}>Historical Order Data</div>
<div className="text-2xl font-bold" style={{ color: '#E0483B' }}>
{orderCount} orders
</div>
</div>
)}
<div className="p-4 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="text-sm mb-2" style={{ color: '#848E9C' }}></div>
<div className="text-lg" style={{ color: '#EAECEF' }}>
<div className="p-4 rounded" style={{ background: '#F7F4EC', border: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="text-sm mb-2" style={{ color: '#8A8478' }}>Status</div>
<div className="text-lg" style={{ color: '#1A1813' }}>
Data fetched successfully, chart component in development
</div>
</div>
</>

View File

@@ -191,11 +191,11 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
<div className="flex flex-col items-center justify-center py-20">
<div className="relative">
<div className="w-16 h-16 border-4 border-t-transparent rounded-full animate-spin"
style={{ borderColor: '#F0B90B', borderTopColor: 'transparent' }} />
style={{ borderColor: '#E0483B', borderTopColor: 'transparent' }} />
<TrendingUp className="w-6 h-6 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
style={{ color: '#F0B90B' }} />
style={{ color: '#E0483B' }} />
</div>
<div className="text-sm mt-4 font-medium" style={{ color: '#848E9C' }}>
<div className="text-sm mt-4 font-medium" style={{ color: '#8A8478' }}>
{t('loadingChartData', language) || 'Loading chart data...'}
</div>
</div>
@@ -206,13 +206,13 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
return (
<div className="flex flex-col items-center justify-center py-20">
<div className="w-20 h-20 rounded-2xl flex items-center justify-center mb-4"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}>
<BarChart3 className="w-10 h-10" style={{ color: '#F0B90B', opacity: 0.6 }} />
style={{ background: 'rgba(224, 72, 59, 0.1)' }}>
<BarChart3 className="w-10 h-10" style={{ color: '#E0483B', opacity: 0.6 }} />
</div>
<div className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>
<div className="text-lg font-bold mb-2" style={{ color: '#1A1813' }}>
{t('noHistoricalData', language)}
</div>
<div className="text-sm text-center max-w-xs" style={{ color: '#848E9C' }}>
<div className="text-sm text-center max-w-xs" style={{ color: '#8A8478' }}>
{t('dataWillAppear', language)}
</div>
</div>
@@ -264,14 +264,14 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
<div
className="rounded-xl p-4 shadow-2xl backdrop-blur-sm"
style={{
background: 'rgba(30, 35, 41, 0.95)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: 'rgba(247, 244, 236, 0.95)',
border: '1px solid rgba(224, 72, 59, 0.2)',
minWidth: '200px'
}}
>
<div className="flex items-center gap-2 mb-3 pb-2" style={{ borderBottom: '1px solid #2B3139' }}>
<Zap className="w-3.5 h-3.5" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#F0B90B' }}>
<div className="flex items-center gap-2 mb-3 pb-2" style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}>
<Zap className="w-3.5 h-3.5" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#E0483B' }}>
{dateStr} {data.time}
</span>
</div>
@@ -288,17 +288,17 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
<div className="w-2.5 h-2.5 rounded-full"
style={{ background: traderColor(trader.trader_id) }} />
<span className="text-xs font-medium truncate max-w-[100px]"
style={{ color: '#EAECEF' }}>
style={{ color: '#1A1813' }}>
{trader.trader_name}
</span>
</div>
<div className="text-right">
<div className="text-sm font-bold mono flex items-center gap-1"
style={{ color: isPositive ? '#0ECB81' : '#F6465D' }}>
style={{ color: isPositive ? '#2E8B57' : '#D6433A' }}>
{isPositive ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
{isPositive ? '+' : ''}{pnlPct.toFixed(2)}%
</div>
<div className="text-[10px] mono" style={{ color: '#5E6673' }}>
<div className="text-[10px] mono" style={{ color: '#8A8478' }}>
${equity?.toFixed(2)}
</div>
</div>
@@ -346,10 +346,10 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
className="px-3 py-1.5 text-xs font-medium rounded-lg transition-all"
style={{
background: selectedPeriod === period.key
? 'rgba(240, 185, 11, 0.2)'
: 'rgba(43, 49, 57, 0.5)',
color: selectedPeriod === period.key ? '#F0B90B' : '#848E9C',
border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
? 'rgba(224, 72, 59, 0.15)'
: 'rgba(26, 24, 19, 0.04)',
color: selectedPeriod === period.key ? '#E0483B' : '#8A8478',
border: `1px solid ${selectedPeriod === period.key ? 'rgba(224, 72, 59, 0.4)' : 'rgba(26, 24, 19, 0.14)'}`,
}}
>
{t(`comparisonChart.${period.key}`, language)}
@@ -363,17 +363,17 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
<div key={trader.trader_id}
className="flex items-center gap-2 px-3 py-1.5 rounded-full transition-all hover:scale-105"
style={{
background: idx === 0 ? 'rgba(240, 185, 11, 0.15)' : 'rgba(43, 49, 57, 0.5)',
border: `1px solid ${idx === 0 ? 'rgba(240, 185, 11, 0.3)' : '#2B3139'}`
background: idx === 0 ? 'rgba(224, 72, 59, 0.15)' : 'rgba(26, 24, 19, 0.04)',
border: `1px solid ${idx === 0 ? 'rgba(224, 72, 59, 0.3)' : 'rgba(26, 24, 19, 0.14)'}`
}}>
<div className="w-2 h-2 rounded-full"
style={{ background: traderColor(trader.trader_id) }} />
<span className="text-xs font-medium truncate max-w-[80px]"
style={{ color: '#EAECEF' }}>
style={{ color: '#1A1813' }}>
{trader.trader_name}
</span>
<span className="text-xs font-bold mono"
style={{ color: trader.currentPnl >= 0 ? '#0ECB81' : '#F6465D' }}>
style={{ color: trader.currentPnl >= 0 ? '#2E8B57' : '#D6433A' }}>
{trader.currentPnl >= 0 ? '+' : ''}{trader.currentPnl.toFixed(2)}%
</span>
</div>
@@ -383,7 +383,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Chart */}
<div className="relative rounded-xl overflow-hidden"
style={{ background: 'linear-gradient(180deg, rgba(11, 14, 17, 0.8) 0%, rgba(11, 14, 17, 1) 100%)' }}>
style={{ background: '#F1ECE2' }}>
{/* Watermark */}
<div style={{
position: 'absolute',
@@ -392,7 +392,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
transform: 'translate(-50%, -50%)',
fontSize: '80px',
fontWeight: 'bold',
color: 'rgba(240, 185, 11, 0.03)',
color: 'rgba(224, 72, 59, 0.04)',
zIndex: 1,
pointerEvents: 'none',
fontFamily: 'monospace',
@@ -427,20 +427,20 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
</filter>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1E2329" vertical={false} />
<CartesianGrid strokeDasharray="3 3" stroke="rgba(26, 24, 19, 0.10)" vertical={false} />
<XAxis
dataKey="time"
stroke="#2B3139"
tick={{ fill: '#5E6673', fontSize: 10 }}
stroke="#6B6557"
tick={{ fill: '#6B6557', fontSize: 10 }}
tickLine={false}
axisLine={{ stroke: '#2B3139' }}
axisLine={{ stroke: 'rgba(26, 24, 19, 0.14)' }}
interval={Math.max(Math.floor(displayData.length / 8), 1)}
/>
<YAxis
stroke="#2B3139"
tick={{ fill: '#5E6673', fontSize: 10 }}
stroke="#6B6557"
tick={{ fill: '#6B6557', fontSize: 10 }}
tickLine={false}
axisLine={false}
domain={calculateYDomain()}
@@ -453,7 +453,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Zero reference line */}
<ReferenceLine
y={0}
stroke="#474D57"
stroke="rgba(26, 24, 19, 0.2)"
strokeDasharray="8 4"
strokeWidth={1}
/>
@@ -482,13 +482,11 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
activeDot={{
r: 6,
fill: traderColor(trader.trader_id),
stroke: '#0B0E11',
stroke: '#F1ECE2',
strokeWidth: 2,
filter: 'url(#glow)',
}}
name={trader.trader_name}
connectNulls
style={{ filter: idx === 0 ? 'url(#glow)' : undefined }}
/>
))}
@@ -515,10 +513,10 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
borderRadius: '50%',
backgroundColor: entry.color
}} />
<span style={{ color: '#EAECEF', fontSize: '12px', fontWeight: 500 }}>
<span style={{ color: '#1A1813', fontSize: '12px', fontWeight: 500 }}>
{entry.value}
<span style={{
color: pnl >= 0 ? '#0ECB81' : '#F6465D',
color: pnl >= 0 ? '#2E8B57' : '#D6433A',
marginLeft: '6px',
fontFamily: 'monospace'
}}>
@@ -539,36 +537,36 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Bottom Stats */}
<div className="grid grid-cols-4 gap-2">
<div className="p-3 rounded-lg text-center"
style={{ background: 'rgba(240, 185, 11, 0.05)', border: '1px solid rgba(240, 185, 11, 0.1)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#848E9C' }}>
style={{ background: 'rgba(224, 72, 59, 0.05)', border: '1px solid rgba(224, 72, 59, 0.1)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#8A8478' }}>
{t('leader', language)}
</div>
<div className="text-sm font-bold truncate" style={{ color: '#F0B90B' }}>
<div className="text-sm font-bold truncate" style={{ color: '#E0483B' }}>
{leader?.trader_name || '-'}
</div>
</div>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(14, 203, 129, 0.05)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#848E9C' }}>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(46, 139, 87, 0.05)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#8A8478' }}>
{t('leadPnL', language) || 'Lead PnL'}
</div>
<div className="text-sm font-bold mono"
style={{ color: (leader?.currentPnl || 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
style={{ color: (leader?.currentPnl || 0) >= 0 ? '#2E8B57' : '#D6433A' }}>
{(leader?.currentPnl || 0) >= 0 ? '+' : ''}{(leader?.currentPnl || 0).toFixed(2)}%
</div>
</div>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(96, 165, 250, 0.05)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#848E9C' }}>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(26, 24, 19, 0.04)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#8A8478' }}>
{t('currentGap', language)}
</div>
<div className="text-sm font-bold mono" style={{ color: '#60a5fa' }}>
<div className="text-sm font-bold mono" style={{ color: '#1A1813' }}>
{gap}%
</div>
</div>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(139, 92, 246, 0.05)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#848E9C' }}>
<div className="p-3 rounded-lg text-center" style={{ background: 'rgba(26, 24, 19, 0.04)' }}>
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: '#8A8478' }}>
{t('dataPoints', language)}
</div>
<div className="text-sm font-bold mono" style={{ color: '#8b5cf6' }}>
<div className="text-sm font-bold mono" style={{ color: '#1A1813' }}>
{displayData.length}
</div>
</div>

View File

@@ -33,7 +33,7 @@ interface EquityPoint {
interface EquityChartProps {
traderId?: string
embedded?: boolean // 嵌入模式(不显示外层卡片)
embedded?: boolean // Embedded mode (does not show the outer card)
}
export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
@@ -45,7 +45,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
user && token && traderId ? `equity-history-${traderId}` : null,
() => api.getEquityHistory(traderId, true),
{
refreshInterval: 30000, // 30秒刷新历史数据更新频率较低
refreshInterval: 30000, // Refresh every 30s (historical data updates less frequently)
revalidateOnFocus: false,
dedupingInterval: 20000,
}
@@ -55,7 +55,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
user && token && traderId ? `account-${traderId}` : null,
() => api.getAccount(traderId, true),
{
refreshInterval: 15000, // 15秒刷新配合后端缓存
refreshInterval: 15000, // Refresh every 15s (matches backend cache)
revalidateOnFocus: false,
dedupingInterval: 10000,
}
@@ -66,7 +66,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
return (
<div className={embedded ? 'p-6' : 'binance-card p-6'}>
{!embedded && (
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>
<h3 className="text-lg font-semibold mb-6" style={{ color: '#1A1813' }}>
{t('accountEquityCurve', language)}
</h3>
)}
@@ -83,16 +83,16 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
<div
className="flex items-center gap-3 p-4 rounded"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.2)',
background: 'rgba(214, 67, 58, 0.1)',
border: '1px solid rgba(214, 67, 58, 0.2)',
}}
>
<AlertTriangle className="w-6 h-6" style={{ color: '#F6465D' }} />
<AlertTriangle className="w-6 h-6" style={{ color: '#D6433A' }} />
<div>
<div className="font-semibold" style={{ color: '#F6465D' }}>
<div className="font-semibold" style={{ color: '#D6433A' }}>
{t('loadingError', language)}
</div>
<div className="text-sm" style={{ color: '#848E9C' }}>
<div className="text-sm" style={{ color: '#8A8478' }}>
{error.message}
</div>
</div>
@@ -101,18 +101,18 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
)
}
// 过滤掉无效数据total_equity为0或小于1的数据点API失败导致
// Filter out invalid data: points where total_equity is 0 or less than 1 (caused by API failures)
const validHistory = history?.filter((point) => point.total_equity > 1) || []
if (!validHistory || validHistory.length === 0) {
return (
<div className={embedded ? 'p-6' : 'binance-card p-6'}>
{!embedded && (
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>
<h3 className="text-lg font-semibold mb-6" style={{ color: '#1A1813' }}>
{t('accountEquityCurve', language)}
</h3>
)}
<div className="text-center py-16" style={{ color: '#848E9C' }}>
<div className="text-center py-16" style={{ color: '#8A8478' }}>
<div className="mb-4 flex justify-center opacity-50">
<BarChart3 className="w-16 h-16" />
</div>
@@ -125,23 +125,23 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
)
}
// 限制显示最近的数据点(性能优化)
// 如果数据超过2000个点只显示最近2000
// Limit to the most recent data points (performance optimization)
// If there are more than 2000 points, only show the most recent 2000
const MAX_DISPLAY_POINTS = 2000
const displayHistory =
validHistory.length > MAX_DISPLAY_POINTS
? validHistory.slice(-MAX_DISPLAY_POINTS)
: validHistory
// 计算初始余额(优先从 account 获取配置的初始余额,备选从历史数据反推)
// Compute the initial balance (prefer the configured value from account, fall back to deriving from history)
const initialBalance =
account?.initial_balance || // 从交易员配置读取真实初始余额
account?.initial_balance || // Read the real initial balance from the trader config
(validHistory[0]
? validHistory[0].total_equity - validHistory[0].pnl
: undefined) || // 备选:淨值 - 盈亏
1000 // 默认值(与创建交易员时的默认配置一致)
: undefined) || // Fallback: equity - pnl
1000 // Default value (matches the default config used when creating a trader)
// 转换数据格式
// Transform the data format
const chartData = displayHistory.map((point, index) => {
const pnl = point.total_equity - initialBalance
const pnlPct = ((pnl / initialBalance) * 100).toFixed(2)
@@ -161,45 +161,45 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
const currentValue = chartData[chartData.length - 1]
const isProfit = currentValue.raw_pnl >= 0
// 计算Y轴范围
// Compute the Y-axis range
const calculateYDomain = () => {
if (displayMode === 'percent') {
// 百分比模式找到最大最小值留20%余量
// Percent mode: find the min/max values, leave a 20% margin
const values = chartData.map((d) => d.value)
const minVal = Math.min(...values)
const maxVal = Math.max(...values)
const range = Math.max(Math.abs(maxVal), Math.abs(minVal))
const padding = Math.max(range * 0.2, 1) // 至少留1%余量
const padding = Math.max(range * 0.2, 1) // Leave at least a 1% margin
return [Math.floor(minVal - padding), Math.ceil(maxVal + padding)]
} else {
// 美元模式以初始余额为基准上下留10%余量
// Dollar mode: anchor on the initial balance, leave a 10% margin above and below
const values = chartData.map((d) => d.value)
const minVal = Math.min(...values, initialBalance)
const maxVal = Math.max(...values, initialBalance)
const range = maxVal - minVal
const padding = Math.max(range * 0.15, initialBalance * 0.01) // 至少留1%余量
const padding = Math.max(range * 0.15, initialBalance * 0.01) // Leave at least a 1% margin
return [Math.floor(minVal - padding), Math.ceil(maxVal + padding)]
}
}
// 自定义Tooltip - Binance Style
// Custom Tooltip - Binance Style
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<div
className="rounded p-3 shadow-xl"
style={{ background: '#1E2329', border: '1px solid #2B3139' }}
style={{ background: '#F7F4EC', border: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
<div className="text-xs mb-1" style={{ color: '#8A8478' }}>
Cycle #{data.cycle != null ? data.cycle : '—'}
</div>
<div className="font-bold mono" style={{ color: '#EAECEF' }}>
<div className="font-bold mono" style={{ color: '#1A1813' }}>
{data.raw_equity.toFixed(2)} USDT
</div>
<div
className="text-sm mono font-bold"
style={{ color: data.raw_pnl >= 0 ? '#0ECB81' : '#F6465D' }}
style={{ color: data.raw_pnl >= 0 ? '#2E8B57' : '#D6433A' }}
>
{data.raw_pnl >= 0 ? '+' : ''}
{data.raw_pnl.toFixed(2)} USDT ({data.raw_pnl_pct >= 0 ? '+' : ''}
@@ -219,7 +219,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
{!embedded && (
<h3
className="text-base sm:text-lg font-bold mb-2"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{t('accountEquityCurve', language)}
</h3>
@@ -227,12 +227,12 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
<div className="flex flex-col sm:flex-row sm:items-baseline gap-2 sm:gap-4">
<span
className="text-2xl sm:text-3xl font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{account?.total_equity.toFixed(2) || '0.00'}
<span
className="text-base sm:text-lg ml-1"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
USDT
</span>
@@ -241,14 +241,14 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
<span
className="text-sm sm:text-lg font-bold mono px-2 sm:px-3 py-1 rounded flex items-center gap-1"
style={{
color: isProfit ? '#0ECB81' : '#F6465D',
color: isProfit ? '#2E8B57' : '#D6433A',
background: isProfit
? 'rgba(14, 203, 129, 0.1)'
: 'rgba(246, 70, 93, 0.1)',
? 'rgba(46, 139, 87, 0.1)'
: 'rgba(214, 67, 58, 0.1)',
border: `1px solid ${
isProfit
? 'rgba(14, 203, 129, 0.2)'
: 'rgba(246, 70, 93, 0.2)'
? 'rgba(46, 139, 87, 0.2)'
: 'rgba(214, 67, 58, 0.2)'
}`,
}}
>
@@ -262,7 +262,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
</span>
<span
className="text-xs sm:text-sm mono"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
({isProfit ? '+' : ''}
{currentValue.raw_pnl.toFixed(2)} USDT)
@@ -274,7 +274,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
{/* Display Mode Toggle */}
<div
className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1 self-start sm:self-auto"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#E8E2D5', border: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<button
onClick={() => setDisplayMode('dollar')}
@@ -282,11 +282,10 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
style={
displayMode === 'dollar'
? {
background: '#F0B90B',
color: '#000',
boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)',
background: '#E0483B',
color: '#F1ECE2',
}
: { background: 'transparent', color: '#848E9C' }
: { background: 'transparent', color: '#8A8478' }
}
>
<DollarSign className="w-4 h-4" /> USDT
@@ -297,11 +296,10 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
style={
displayMode === 'percent'
? {
background: '#F0B90B',
color: '#000',
boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)',
background: '#E0483B',
color: '#F1ECE2',
}
: { background: 'transparent', color: '#848E9C' }
: { background: 'transparent', color: '#8A8478' }
}
>
<Percent className="w-4 h-4" />
@@ -326,7 +324,7 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
right: '15px',
fontSize: '20px',
fontWeight: 'bold',
color: 'rgba(240, 185, 11, 0.15)',
color: 'rgba(224, 72, 59, 0.15)',
zIndex: 10,
pointerEvents: 'none',
fontFamily: 'monospace',
@@ -341,25 +339,25 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
>
<defs>
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#F0B90B" stopOpacity={0.8} />
<stop offset="95%" stopColor="#FCD535" stopOpacity={0.2} />
<stop offset="5%" stopColor="#E0483B" stopOpacity={0.8} />
<stop offset="95%" stopColor="#E0483B" stopOpacity={0.2} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#2B3139" />
<CartesianGrid strokeDasharray="3 3" stroke="rgba(26, 24, 19, 0.10)" />
<XAxis
dataKey="time"
stroke="#5E6673"
tick={{ fill: '#848E9C', fontSize: 11 }}
tickLine={{ stroke: '#2B3139' }}
stroke="#6B6557"
tick={{ fill: '#6B6557', fontSize: 11 }}
tickLine={{ stroke: 'rgba(26, 24, 19, 0.14)' }}
interval={Math.floor(chartData.length / 10)}
angle={-15}
textAnchor="end"
height={60}
/>
<YAxis
stroke="#5E6673"
tick={{ fill: '#848E9C', fontSize: 12 }}
tickLine={{ stroke: '#2B3139' }}
stroke="#6B6557"
tick={{ fill: '#6B6557', fontSize: 12 }}
tickLine={{ stroke: 'rgba(26, 24, 19, 0.14)' }}
domain={calculateYDomain()}
tickFormatter={(value) =>
displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%`
@@ -368,14 +366,14 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
<Tooltip content={<CustomTooltip />} />
<ReferenceLine
y={displayMode === 'dollar' ? initialBalance : 0}
stroke="#474D57"
stroke="rgba(26, 24, 19, 0.2)"
strokeDasharray="3 3"
label={{
value:
displayMode === 'dollar'
? t('initialBalance', language).split(' ')[0]
: '0%',
fill: '#848E9C',
fill: '#8A8478',
fontSize: 12,
}}
/>
@@ -384,11 +382,11 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
dataKey="value"
stroke="url(#colorGradient)"
strokeWidth={3}
dot={chartData.length > 50 ? false : { fill: '#F0B90B', r: 3 }}
dot={chartData.length > 50 ? false : { fill: '#E0483B', r: 3 }}
activeDot={{
r: 6,
fill: '#FCD535',
stroke: '#F0B90B',
fill: '#E0483B',
stroke: '#F1ECE2',
strokeWidth: 2,
}}
connectNulls={true}
@@ -400,72 +398,72 @@ export function EquityChart({ traderId, embedded = false }: EquityChartProps) {
{/* Footer Stats */}
<div
className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 pt-3"
style={{ borderTop: '1px solid #2B3139' }}
style={{ borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<div
className="p-2 rounded transition-all hover:bg-opacity-50"
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
style={{ background: 'rgba(224, 72, 59, 0.05)' }}
>
<div
className="text-xs mb-1 uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('initialBalance', language)}
</div>
<div
className="text-xs sm:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{initialBalance.toFixed(2)} USDT
</div>
</div>
<div
className="p-2 rounded transition-all hover:bg-opacity-50"
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
style={{ background: 'rgba(224, 72, 59, 0.05)' }}
>
<div
className="text-xs mb-1 uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('currentEquity', language)}
</div>
<div
className="text-xs sm:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{currentValue.raw_equity.toFixed(2)} USDT
</div>
</div>
<div
className="p-2 rounded transition-all hover:bg-opacity-50"
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
style={{ background: 'rgba(224, 72, 59, 0.05)' }}
>
<div
className="text-xs mb-1 uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('historicalCycles', language)}
</div>
<div
className="text-xs sm:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{validHistory.length} {t('cycles', language)}
</div>
</div>
<div
className="p-2 rounded transition-all hover:bg-opacity-50"
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
style={{ background: 'rgba(224, 72, 59, 0.05)' }}
>
<div
className="text-xs mb-1 uppercase tracking-wider"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('displayRange', language)}
</div>
<div
className="text-xs sm:text-sm font-bold mono"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{validHistory.length > MAX_DISPLAY_POINTS
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`

View File

@@ -3,7 +3,7 @@ import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
import { ChevronDown, TrendingUp, X } from 'lucide-react'
// 支持的交易所列表 (合约格式)
// Supported exchanges list (futures format)
const EXCHANGES = [
{ id: 'BINANCE', name: 'Binance', prefix: 'BINANCE:', suffix: '.P' },
{ id: 'BYBIT', name: 'Bybit', prefix: 'BYBIT:', suffix: '.P' },
@@ -13,7 +13,7 @@ const EXCHANGES = [
{ id: 'GATEIO', name: 'Gate.io', prefix: 'GATEIO:', suffix: '.P' },
] as const
// 热门交易对
// Popular trading pairs
const POPULAR_SYMBOLS = [
'BTCUSDT',
'ETHUSDT',
@@ -29,7 +29,7 @@ const POPULAR_SYMBOLS = [
'LTCUSDT',
]
// 时间周期选项
// Time interval options
const INTERVALS = [
{ id: '1', label: '1m' },
{ id: '5', label: '5m' },
@@ -46,7 +46,7 @@ interface TradingViewChartProps {
defaultExchange?: string
height?: number
showToolbar?: boolean
embedded?: boolean // 嵌入模式(不显示外层卡片)
embedded?: boolean // Embedded mode (does not show the outer card)
}
function TradingViewChartComponent({
@@ -66,26 +66,26 @@ function TradingViewChartComponent({
const [showSymbolDropdown, setShowSymbolDropdown] = useState(false)
const [isFullscreen, setIsFullscreen] = useState(false)
// 当外部传入的 defaultSymbol 变化时,更新内部 symbol
// Update the internal symbol when the external defaultSymbol changes
useEffect(() => {
if (defaultSymbol && defaultSymbol !== symbol) {
// console.log('[TradingViewChart] 更新币种:', defaultSymbol)
// console.log('[TradingViewChart] Updating symbol:', defaultSymbol)
setSymbol(defaultSymbol)
}
}, [defaultSymbol])
// 当外部传入的 defaultExchange 变化时,更新内部 exchange
// Update the internal exchange when the external defaultExchange changes
useEffect(() => {
if (defaultExchange && defaultExchange !== exchange) {
const normalizedExchange = defaultExchange.toUpperCase()
// console.log('[TradingViewChart] 更新交易所:', normalizedExchange)
// console.log('[TradingViewChart] Updating exchange:', normalizedExchange)
if (EXCHANGES.some(e => e.id === normalizedExchange)) {
setExchange(normalizedExchange)
}
}
}, [defaultExchange])
// 获取完整的交易对符号 (合约格式: BINANCE:BTCUSDT.P)
// Get the full trading pair symbol (futures format: BINANCE:BTCUSDT.P)
const getFullSymbol = () => {
const exchangeInfo = EXCHANGES.find((e) => e.id === exchange)
const prefix = exchangeInfo?.prefix || 'BINANCE:'
@@ -93,14 +93,14 @@ function TradingViewChartComponent({
return `${prefix}${symbol}${suffix}`
}
// 加载 TradingView Widget
// Load the TradingView Widget
useEffect(() => {
if (!containerRef.current) return
// 清空容器
// Clear the container
containerRef.current.innerHTML = ''
// 创建 widget 容器
// Create the widget container
const widgetContainer = document.createElement('div')
widgetContainer.className = 'tradingview-widget-container'
widgetContainer.style.height = '100%'
@@ -114,7 +114,7 @@ function TradingViewChartComponent({
widgetContainer.appendChild(widgetDiv)
containerRef.current.appendChild(widgetContainer)
// 加载 TradingView 脚本
// Load the TradingView script
const script = document.createElement('script')
script.src =
'https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js'
@@ -126,12 +126,12 @@ function TradingViewChartComponent({
symbol: getFullSymbol(),
interval: timeInterval,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai',
theme: 'dark',
theme: 'light',
style: '1',
locale: language === 'zh' ? 'zh_CN' : 'en',
enable_publishing: false,
backgroundColor: 'rgba(11, 14, 17, 1)',
gridColor: 'rgba(43, 49, 57, 0.5)',
backgroundColor: 'rgba(241, 236, 226, 1)',
gridColor: 'rgba(26, 24, 19, 0.08)',
hide_top_toolbar: !showToolbar,
hide_legend: false,
save_image: false,
@@ -149,11 +149,11 @@ function TradingViewChartComponent({
}
}, [exchange, symbol, timeInterval, language, showToolbar])
// 处理自定义交易对输入
// Handle custom trading pair input
const handleCustomSymbolSubmit = () => {
if (customSymbol.trim()) {
let sym = customSymbol.trim().toUpperCase()
// 如果没有 USDT 后缀,自动加上
// If there is no USDT suffix, add it automatically
if (!sym.endsWith('USDT')) {
sym = sym + 'USDT'
}
@@ -169,19 +169,19 @@ function TradingViewChartComponent({
? 'fixed inset-0 z-50 rounded-none flex flex-col'
: ''
}`}
style={isFullscreen ? { background: '#0B0E11' } : undefined}
style={isFullscreen ? { background: '#F1ECE2' } : undefined}
>
{/* Header */}
<div
className="flex flex-wrap items-center gap-2 p-3 sm:p-4"
style={{ borderBottom: embedded ? 'none' : '1px solid #2B3139' }}
style={{ borderBottom: embedded ? 'none' : '1px solid rgba(26, 24, 19, 0.14)' }}
>
{!embedded && (
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5" style={{ color: '#F0B90B' }} />
<TrendingUp className="w-5 h-5" style={{ color: '#E0483B' }} />
<h3
className="text-base sm:text-lg font-bold"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{t('marketChart', language)}
</h3>
@@ -199,21 +199,21 @@ function TradingViewChartComponent({
}}
className="flex items-center gap-1 px-3 py-1.5 rounded text-sm font-medium transition-all"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
color: '#1A1813',
}}
>
{EXCHANGES.find((e) => e.id === exchange)?.name || exchange}
<ChevronDown className="w-4 h-4" style={{ color: '#848E9C' }} />
<ChevronDown className="w-4 h-4" style={{ color: '#8A8478' }} />
</button>
{showExchangeDropdown && (
<div
className="absolute top-full left-0 mt-1 py-1 rounded-lg shadow-xl z-20 min-w-[120px]"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
{EXCHANGES.map((ex) => (
@@ -225,10 +225,10 @@ function TradingViewChartComponent({
}}
className="w-full px-4 py-2 text-left text-sm transition-all hover:bg-opacity-50"
style={{
color: exchange === ex.id ? '#F0B90B' : '#EAECEF',
color: exchange === ex.id ? '#E0483B' : '#1A1813',
background:
exchange === ex.id
? 'rgba(240, 185, 11, 0.1)'
? 'rgba(224, 72, 59, 0.1)'
: 'transparent',
}}
>
@@ -248,9 +248,9 @@ function TradingViewChartComponent({
}}
className="flex items-center gap-1 px-3 py-1.5 rounded text-sm font-bold transition-all"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
color: '#F0B90B',
background: 'rgba(224, 72, 59, 0.1)',
border: '1px solid rgba(224, 72, 59, 0.3)',
color: '#E0483B',
}}
>
{symbol}
@@ -261,12 +261,12 @@ function TradingViewChartComponent({
<div
className="absolute top-full left-0 mt-1 py-2 rounded-lg shadow-xl z-20 w-[280px]"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
{/* Custom Input */}
<div className="px-3 pb-2" style={{ borderBottom: '1px solid #2B3139' }}>
<div className="px-3 pb-2" style={{ borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="flex gap-2">
<input
type="text"
@@ -276,17 +276,17 @@ function TradingViewChartComponent({
placeholder={t('enterSymbol', language)}
className="flex-1 px-3 py-1.5 rounded text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
background: '#F1ECE2',
border: '1px solid rgba(26, 24, 19, 0.14)',
color: '#1A1813',
}}
/>
<button
onClick={handleCustomSymbolSubmit}
className="px-3 py-1.5 rounded text-sm font-medium"
style={{
background: '#F0B90B',
color: '#0B0E11',
background: '#E0483B',
color: '#F1ECE2',
}}
>
OK
@@ -298,7 +298,7 @@ function TradingViewChartComponent({
<div className="px-2 pt-2">
<div
className="text-xs px-2 py-1 mb-1"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('popularSymbols', language)}
</div>
@@ -312,11 +312,11 @@ function TradingViewChartComponent({
}}
className="px-2 py-1.5 rounded text-xs font-medium transition-all"
style={{
color: symbol === sym ? '#F0B90B' : '#EAECEF',
color: symbol === sym ? '#E0483B' : '#1A1813',
background:
symbol === sym
? 'rgba(240, 185, 11, 0.1)'
: 'rgba(43, 49, 57, 0.3)',
? 'rgba(224, 72, 59, 0.1)'
: 'rgba(26, 24, 19, 0.04)',
}}
>
{sym.replace('USDT', '')}
@@ -331,7 +331,7 @@ function TradingViewChartComponent({
{/* Interval Selector */}
<div
className="flex gap-0.5 p-0.5 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#E8E2D5', border: '1px solid rgba(26, 24, 19, 0.14)' }}
>
{INTERVALS.map((int) => (
<button
@@ -339,8 +339,8 @@ function TradingViewChartComponent({
onClick={() => setTimeInterval(int.id)}
className="px-2 py-1 rounded text-xs font-medium transition-all"
style={{
background: timeInterval === int.id ? '#F0B90B' : 'transparent',
color: timeInterval === int.id ? '#0B0E11' : '#848E9C',
background: timeInterval === int.id ? '#E0483B' : 'transparent',
color: timeInterval === int.id ? '#F1ECE2' : '#8A8478',
}}
>
{int.label}
@@ -353,9 +353,9 @@ function TradingViewChartComponent({
onClick={() => setIsFullscreen(!isFullscreen)}
className="p-1.5 rounded transition-all"
style={{
background: isFullscreen ? '#F0B90B' : 'transparent',
color: isFullscreen ? '#0B0E11' : '#848E9C',
border: '1px solid #2B3139',
background: isFullscreen ? '#E0483B' : 'transparent',
color: isFullscreen ? '#F1ECE2' : '#8A8478',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
title={isFullscreen ? t('exitFullscreen', language) : t('fullscreen', language)}
>
@@ -375,7 +375,7 @@ function TradingViewChartComponent({
ref={containerRef}
style={{
height: isFullscreen ? 'calc(100vh - 65px)' : height,
background: '#0B0E11',
background: '#F1ECE2',
overflow: 'hidden',
}}
/>
@@ -394,5 +394,5 @@ function TradingViewChartComponent({
)
}
// 使用 memo 避免不必要的重渲染
// Use memo to avoid unnecessary re-renders
export const TradingViewChart = memo(TradingViewChartComponent)

View File

@@ -5,16 +5,16 @@ interface ContainerProps {
className?: string
as?: 'div' | 'main' | 'header' | 'section'
style?: CSSProperties
/** 是否充满宽度(取消 max-width */
/** Whether to fill the full width (removes max-width) */
fluid?: boolean
/** 是否取消水平内边距 */
/** Whether to remove horizontal padding */
noPadding?: boolean
/** 自定义最大宽度类(默认 max-w-[1920px] */
/** Custom max-width class (default max-w-[1920px]) */
maxWidthClass?: string
}
/**
* 统一的容器组件,确保所有页面元素使用一致的最大宽度和内边距
* Unified container component that ensures all page elements use a consistent max width and padding
* - max-width: 1920px
* - padding: 24px (mobile) -> 32px (tablet) -> 48px (desktop)
*/

View File

@@ -9,32 +9,17 @@ interface DeepVoidBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {
export function DeepVoidBackground({ children, className = '', disableAnimation = false, ...props }: DeepVoidBackgroundProps) {
return (
<div className={`relative w-full min-h-screen bg-nofx-bg text-nofx-text overflow-hidden flex flex-col ${className}`} {...props}>
{/* Background layers: use a much lighter static stack when animations are disabled */}
{/* Background layers: neutralized to a plain cream surface for the light theme */}
{disableAnimation ? (
<>
<div className="absolute inset-0 pointer-events-none z-0 bg-[radial-gradient(circle_at_top,rgba(240,185,11,0.08),transparent_38%),linear-gradient(180deg,rgba(12,14,20,0.98),rgba(8,10,15,1))]"></div>
<div className="absolute inset-0 pointer-events-none z-0 opacity-[0.035] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:36px_36px]"></div>
<div className="absolute inset-0 pointer-events-none z-0 bg-nofx-bg"></div>
<div className="absolute inset-0 pointer-events-none z-0 opacity-[0.035] bg-[linear-gradient(to_right,rgba(26,24,19,0.08)_1px,transparent_1px),linear-gradient(to_bottom,rgba(26,24,19,0.08)_1px,transparent_1px)] bg-[size:36px_36px]"></div>
</>
) : (
<>
{/* 1. Grain/Noise Texture */}
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 mix-blend-soft-light pointer-events-none fixed z-0"></div>
{/* 2. Grid System */}
{/* Faint grid system on cream */}
<div className="absolute inset-0 pointer-events-none fixed z-0">
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] opacity-50" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(100px) scale(2)' }}></div>
<div className="absolute inset-0 bg-grid-pattern opacity-[0.03]"></div>
</div>
{/* 3. Ambient Glow Spots */}
<div className="absolute inset-0 overflow-hidden pointer-events-none fixed z-0">
<div className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-nofx-gold/10 rounded-full blur-[120px] mix-blend-screen animate-pulse-slow"></div>
<div className="absolute bottom-[-10%] right-[-10%] w-[40vw] h-[40vw] bg-nofx-accent/5 rounded-full blur-[120px] mix-blend-screen animate-pulse-slow" style={{ animationDelay: '2s' }}></div>
</div>
{/* 4. CRT/Scanline Overlay */}
<div className="absolute inset-0 pointer-events-none fixed z-[9999] opacity-40">
<div className="absolute inset-0 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))] bg-[length:100%_4px,3px_100%] pointer-events-none"></div>
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-[linear-gradient(to_right,rgba(26,24,19,0.07)_1px,transparent_1px),linear-gradient(to_bottom,rgba(26,24,19,0.07)_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] opacity-50" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(100px) scale(2)' }}></div>
</div>
</>
)}

View File

@@ -6,7 +6,7 @@ interface IconProps {
className?: string
}
// 本地图标路径映射
// Local icon path mapping
const ICON_PATHS: Record<string, string> = {
binance: '/exchange-icons/binance.jpg',
bybit: '/exchange-icons/bybit.png',
@@ -20,7 +20,7 @@ const ICON_PATHS: Record<string, string> = {
indodax: '/exchange-icons/indodax.png',
}
// 通用图标组件
// Generic icon component
const ExchangeImage: React.FC<IconProps & { src: string; alt: string }> = ({
width = 24,
height = 24,
@@ -36,7 +36,7 @@ const ExchangeImage: React.FC<IconProps & { src: string; alt: string }> = ({
borderRadius: 6,
overflow: 'hidden',
flexShrink: 0,
background: '#2B3139',
background: '#E8E2D5',
}}
>
<img
@@ -51,7 +51,7 @@ const ExchangeImage: React.FC<IconProps & { src: string; alt: string }> = ({
</div>
)
// Fallback 图标
// Fallback icon
const FallbackIcon: React.FC<IconProps & { label: string }> = ({
width = 24,
height = 24,
@@ -64,13 +64,13 @@ const FallbackIcon: React.FC<IconProps & { label: string }> = ({
width,
height,
borderRadius: 6,
background: '#2B3139',
background: '#E8E2D5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: Math.max(10, (width || 24) * 0.4),
fontWeight: 'bold',
color: '#EAECEF',
color: '#1A1813',
flexShrink: 0,
}}
>
@@ -78,7 +78,7 @@ const FallbackIcon: React.FC<IconProps & { label: string }> = ({
</div>
)
// 获取交易所图标的函数
// Returns the icon for an exchange
export const getExchangeIcon = (
exchangeType: string,
props: IconProps = {}

View File

@@ -19,11 +19,11 @@ export function Header({ simple = false }: HeaderProps) {
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-8 h-8" />
</div>
<div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<h1 className="text-xl font-bold" style={{ color: '#1A1813' }}>
{t('appTitle', language)}
</h1>
{!simple && (
<p className="text-xs mono" style={{ color: '#848E9C' }}>
<p className="text-xs mono" style={{ color: '#8A8478' }}>
{t('subtitle', language)}
</p>
)}
@@ -33,26 +33,26 @@ export function Header({ simple = false }: HeaderProps) {
{/* Right - Language Toggle (always show) */}
<div
className="flex gap-1 rounded p-1"
style={{ background: '#1E2329' }}
style={{ background: '#E8E2D5' }}
>
<button
onClick={() => setLanguage('zh')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={
language === 'zh'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
? { background: '#E0483B', color: '#F1ECE2' }
: { background: 'transparent', color: '#8A8478' }
}
>
Chinese
</button>
<button
onClick={() => setLanguage('en')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={
language === 'en'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
? { background: '#E0483B', color: '#F1ECE2' }
: { background: 'transparent', color: '#8A8478' }
}
>
EN
@@ -62,8 +62,8 @@ export function Header({ simple = false }: HeaderProps) {
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={
language === 'id'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
? { background: '#E0483B', color: '#F1ECE2' }
: { background: 'transparent', color: '#8A8478' }
}
>
ID

View File

@@ -93,8 +93,10 @@ export default function HeaderBar({
}}
className="flex items-center gap-2 hover:opacity-80 transition-opacity cursor-pointer"
>
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-7 h-7" />
<span className="text-lg font-bold text-nofx-gold">NOFX</span>
<span className="flex items-center justify-center w-8 h-8 rounded-md overflow-hidden shrink-0" style={{ background: '#fff', border: '1px solid rgba(26,24,19,0.12)' }}>
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
</span>
<span className="text-lg font-bold text-nofx-gold tracking-wide">NOFX</span>
</div>
{/* Desktop Menu */}
@@ -117,7 +119,7 @@ export default function HeaderBar({
path: ROUTES.data,
label:
language === 'zh'
? '数据'
? 'Data'
: language === 'id'
? 'Data'
: 'Data',
@@ -128,7 +130,7 @@ export default function HeaderBar({
path: ROUTES.strategyMarket,
label:
language === 'zh'
? '策略市场'
? 'Market'
: language === 'id'
? 'Pasar'
: 'Market',
@@ -201,6 +203,8 @@ export default function HeaderBar({
</button>
))
})()}
{/* Dashboard context slot — terminal selector + status portals in here */}
<div id="dash-header-slot" className="hidden lg:flex items-center" />
</div>
{/* Right Side - Social Links and User Actions */}
@@ -216,7 +220,7 @@ export default function HeaderBar({
href={OFFICIAL_LINKS.github}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-white hover:bg-white/5"
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-nofx-text hover:bg-[rgba(26,24,19,0.06)]"
title="GitHub"
>
<svg
@@ -265,7 +269,7 @@ export default function HeaderBar({
</div>
{/* Divider */}
<div className="h-5 w-px" style={{ background: '#2B3139' }} />
<div className="h-5 w-px" style={{ background: 'rgba(26,24,19,0.15)' }} />
{/* User Info and Actions */}
{isLoggedIn && user ? (
@@ -274,9 +278,9 @@ export default function HeaderBar({
<div className="relative" ref={userDropdownRef}>
<button
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
className="flex items-center gap-2 px-3 py-2 rounded transition-colors bg-nofx-bg-lighter border border-nofx-gold/20 hover:bg-white/5"
className="flex items-center gap-2 px-3 py-2 rounded transition-colors bg-nofx-bg-lighter border border-nofx-gold/20 hover:bg-[rgba(26,24,19,0.06)]"
>
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-black">
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-white">
{user.email[0].toUpperCase()}
</div>
<span className="text-sm text-nofx-text-muted">
@@ -300,7 +304,7 @@ export default function HeaderBar({
navigateInApp(ROUTES.settings)
setUserDropdownOpen(false)
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-white/5 text-nofx-text-muted hover:text-white"
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-[rgba(26,24,19,0.06)] text-nofx-text-muted hover:text-nofx-text"
>
<Settings className="w-3.5 h-3.5" />
Settings
@@ -311,15 +315,15 @@ export default function HeaderBar({
userMode === 'beginner' ? 'advanced' : 'beginner'
)
}
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-white/5 text-nofx-text-muted hover:text-white"
className="w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors hover:bg-[rgba(26,24,19,0.06)] text-nofx-text-muted hover:text-nofx-text"
>
<Settings className="w-3.5 h-3.5" />
{userMode === 'beginner'
? language === 'zh'
? '切到老手模式'
? 'Switch to Advanced'
: 'Switch to Advanced'
: language === 'zh'
? '切到新手模式'
? 'Switch to Beginner'
: 'Switch to Beginner'}
</button>
{onLogout && (
@@ -345,7 +349,7 @@ export default function HeaderBar({
<button
type="button"
onClick={() => navigateInApp(ROUTES.login)}
className="px-3 py-2 text-sm font-medium transition-colors rounded text-nofx-text-muted hover:text-white"
className="px-3 py-2 text-sm font-medium transition-colors rounded text-nofx-text-muted hover:text-nofx-text"
>
{t('signIn', language)}
</button>
@@ -372,19 +376,19 @@ export default function HeaderBar({
onLanguageChange?.('zh')
setLanguageDropdownOpen(false)
}}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
${language === 'zh' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
${language === 'zh' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
>
<span className="text-base">🇨🇳</span>
<span className="text-sm"></span>
<span className="text-sm">Chinese</span>
</button>
<button
onClick={() => {
onLanguageChange?.('en')
setLanguageDropdownOpen(false)
}}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
${language === 'en' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
${language === 'en' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
>
<span className="text-base">🇺🇸</span>
<span className="text-sm">English</span>
@@ -394,8 +398,8 @@ export default function HeaderBar({
onLanguageChange?.('id')
setLanguageDropdownOpen(false)
}}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
${language === 'id' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-nofx-text
${language === 'id' ? 'bg-nofx-gold/10' : 'hover:bg-[rgba(26,24,19,0.06)]'}`}
>
<span className="text-base">🇮🇩</span>
<span className="text-sm">Bahasa</span>
@@ -409,7 +413,7 @@ export default function HeaderBar({
{/* Mobile Menu Button */}
<motion.button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden text-nofx-text-muted hover:text-white"
className="md:hidden text-nofx-text-muted hover:text-nofx-text"
whileTap={{ scale: 0.9 }}
>
{mobileMenuOpen ? (
@@ -453,7 +457,7 @@ export default function HeaderBar({
path: ROUTES.data,
label:
language === 'zh'
? '数据'
? 'Data'
: language === 'id'
? 'Data'
: 'Data',
@@ -464,7 +468,7 @@ export default function HeaderBar({
path: ROUTES.strategyMarket,
label:
language === 'zh'
? '策略市场'
? 'Market'
: language === 'id'
? 'Pasar'
: 'Market',
@@ -657,7 +661,7 @@ export default function HeaderBar({
navigateInApp(ROUTES.login)
setMobileMenuOpen(false)
}}
className="flex items-center justify-center bg-nofx-gold text-black rounded-lg font-bold text-sm hover:bg-yellow-400 transition-colors"
className="flex items-center justify-center bg-nofx-gold text-white rounded-lg font-bold text-sm hover:opacity-90 transition-colors"
>
{t('signIn', language)}
</button>

View File

@@ -76,7 +76,7 @@ function buildAgentName(nowMs: number) {
return `${AGENT_NAME} valid_until ${nowMs + AGENT_VALIDITY_MS}`
}
const HYPERLIQUID_BUILDER_ADDRESS = '0x891dc6f05ad47a3c1a05da55e7a7517971faaf0d'
// 0.05% (5). Must match the server's defaultHyperliquidBuilderMaxFee and
// 0.05% (5 bps). Must match the server's defaultHyperliquidBuilderMaxFee and
// the BuilderInfo.Fee=50 (= 5 bps) used at order placement. The user signs
// this exact string when approving the builder during wallet connect.
const HYPERLIQUID_BUILDER_MAX_FEE = '0.05%'
@@ -131,7 +131,7 @@ function getPreferredWalletProvider(): WalletProvider | undefined {
function walletSupportLabel(language: Language) {
return language === 'zh'
? '支持 MetaMaskRabbyCoinbasePhantomBraveBackpackOKXTrust 等 EVM 钱包。'
? 'Supports MetaMask, Rabby, Coinbase, Phantom, Brave, Backpack, OKX, Trust and other EVM wallets.'
: 'Supports MetaMask, Rabby, Coinbase Wallet, Phantom, Brave, Backpack, OKX, Trust and other EVM wallets.'
}
@@ -241,53 +241,53 @@ export function HyperliquidWalletConnect({
const [hasWalletProvider, setHasWalletProvider] = useState(false)
const text = useMemo(
() => ({
title: language === 'zh' ? 'Hyperliquid 钱包' : 'Hyperliquid Wallet',
connect: language === 'zh' ? '连接 Hyperliquid' : 'Connect Hyperliquid',
connected: language === 'zh' ? '已连接' : 'Connected',
mainWallet: language === 'zh' ? 'EVM 主钱包' : 'EVM main wallet',
title: language === 'zh' ? 'Hyperliquid Wallet' : 'Hyperliquid Wallet',
connect: language === 'zh' ? 'Connect Hyperliquid' : 'Connect Hyperliquid',
connected: language === 'zh' ? 'Connected' : 'Connected',
mainWallet: language === 'zh' ? 'EVM main wallet' : 'EVM main wallet',
generateAgent:
language === 'zh'
? '生成 NOFX Agent 钱包'
? 'Generate NOFX agent wallet'
: 'Generate NOFX agent wallet',
approveAgent:
language === 'zh' ? '授权 Agent 交易' : 'Authorize agent trading',
language === 'zh' ? 'Authorize agent trading' : 'Authorize agent trading',
approveBuilder:
language === 'zh' ? '完成交易授权' : 'Finalize trading authorization',
save: language === 'zh' ? '保存到 NOFX' : 'Save to NOFX',
done: language === 'zh' ? '流程已完成' : 'Flow complete',
balance: language === 'zh' ? 'Hyperliquid 余额' : 'Hyperliquid balance',
withdrawable: language === 'zh' ? '可用' : 'Withdrawable',
equity: language === 'zh' ? '权益' : 'Equity',
marginUsed: language === 'zh' ? '已用保证金' : 'Margin used',
unrealizedPnl: language === 'zh' ? '未实现盈亏' : 'Unrealized PnL',
refresh: language === 'zh' ? '刷新' : 'Refresh',
language === 'zh' ? 'Finalize trading authorization' : 'Finalize trading authorization',
save: language === 'zh' ? 'Save to NOFX' : 'Save to NOFX',
done: language === 'zh' ? 'Flow complete' : 'Flow complete',
balance: language === 'zh' ? 'Hyperliquid balance' : 'Hyperliquid balance',
withdrawable: language === 'zh' ? 'Withdrawable' : 'Withdrawable',
equity: language === 'zh' ? 'Equity' : 'Equity',
marginUsed: language === 'zh' ? 'Margin used' : 'Margin used',
unrealizedPnl: language === 'zh' ? 'Unrealized PnL' : 'Unrealized PnL',
refresh: language === 'zh' ? 'Refresh' : 'Refresh',
noCustody:
language === 'zh'
? '资金保留在你的 Hyperliquid 账户NOFX 只保存已授权 Agent 钱包。'
? 'Funds stay in your Hyperliquid account; NOFX only stores the authorized agent wallet.'
: 'Funds stay in your Hyperliquid account; NOFX only stores the authorized agent wallet.',
agentExpiry:
language === 'zh' ? 'Agent 授权到期' : 'Agent authorization expires',
agentExpired: language === 'zh' ? '已过期' : 'Expired',
language === 'zh' ? 'Agent authorization expires' : 'Agent authorization expires',
agentExpired: language === 'zh' ? 'Expired' : 'Expired',
agentNoAuth:
language === 'zh'
? '未检测到 NOFX Agent 授权'
? 'No NOFX agent authorization found'
: 'No NOFX agent authorization found',
renewAgent:
language === 'zh'
? '续期 Agent 授权(+180 天)'
? 'Renew agent authorization (+180d)'
: 'Renew agent authorization (+180d)',
renewHint:
language === 'zh'
? 'Hyperliquid 不允许重复使用同一个 Agent续期会生成一个新的 Agent 并以 180 天有效期授权,然后自动更新 NOFX 保存的私钥(需登录)。'
? 'Hyperliquid forbids reusing an agent, so renewal creates a new agent approved for 180 days, then updates the stored key in NOFX (sign-in required).'
: 'Hyperliquid forbids reusing an agent, so renewal creates a new agent approved for 180 days, then updates the stored key in NOFX (sign-in required).',
noWalletTitle:
language === 'zh' ? '未检测到 EVM 钱包' : 'No EVM wallet detected',
language === 'zh' ? 'No EVM wallet detected' : 'No EVM wallet detected',
noWalletDetail:
language === 'zh'
? '先安装 Rabby MetaMask,创建或导入钱包,然后回到这里连接 Hyperliquid'
? 'Install Rabby or MetaMask, create or import a wallet, then return here to connect Hyperliquid.'
: 'Install Rabby or MetaMask, create or import a wallet, then return here to connect Hyperliquid.',
installRabby: language === 'zh' ? '安装 Rabby' : 'Install Rabby',
installMetaMask: language === 'zh' ? '安装 MetaMask' : 'Install MetaMask',
installRabby: language === 'zh' ? 'Install Rabby' : 'Install Rabby',
installMetaMask: language === 'zh' ? 'Install MetaMask' : 'Install MetaMask',
}),
[language]
)
@@ -500,7 +500,7 @@ export function HyperliquidWalletConnect({
if (!provider) {
setError(
language === 'zh'
? '未检测到 EVM 钱包,请安装 MetaMask / Rabby / OKX / Coinbase Wallet'
? 'No EVM wallet detected. Install MetaMask, Rabby, OKX or Coinbase Wallet.'
: 'No EVM wallet detected. Install MetaMask, Rabby, OKX or Coinbase Wallet.'
)
return
@@ -625,7 +625,7 @@ export function HyperliquidWalletConnect({
if (!isLoggedIn) {
setError(
language === 'zh'
? '续期需要先登录 NOFXHyperliquid 不允许重复使用同一个 Agent续期会生成新的 Agent 并更新已保存的私钥。'
? 'Renewal requires signing in: Hyperliquid forbids reusing the same agent, so renewal creates a new agent and updates the stored key.'
: 'Renewal requires signing in: Hyperliquid forbids reusing the same agent, so renewal creates a new agent and updates the stored key.'
)
return
@@ -680,7 +680,7 @@ export function HyperliquidWalletConnect({
}))
throw new Error(
language === 'zh'
? '新 Agent 已授权,但未找到对应的 NOFX 配置,请用“保存到 NOFX”手动保存。'
? 'New agent approved, but no matching NOFX config was found. Use "Save to NOFX" to store it.'
: 'New agent approved, but no matching NOFX config was found. Use "Save to NOFX" to store it.'
)
}
@@ -712,7 +712,7 @@ export function HyperliquidWalletConnect({
}))
toast.success(
language === 'zh'
? 'Agent 已续期(新 Agent有效期 180 天)'
? 'Agent renewed (new agent, valid 180 days)'
: 'Agent renewed (new agent, valid 180 days)'
)
await refreshAgentInfo()
@@ -721,7 +721,7 @@ export function HyperliquidWalletConnect({
err instanceof Error
? err.message
: language === 'zh'
? 'Agent 续期失败'
? 'Agent renewal failed'
: 'Agent renewal failed'
)
} finally {
@@ -773,14 +773,14 @@ export function HyperliquidWalletConnect({
: undefined,
}))
toast.success(
language === 'zh' ? '交易授权已完成' : 'Trading authorization finalized'
language === 'zh' ? 'Trading authorization finalized' : 'Trading authorization finalized'
)
} catch (err) {
setError(
err instanceof Error
? err.message
: language === 'zh'
? '交易授权失败'
? 'Trading authorization failed'
: 'Trading authorization failed'
)
} finally {
@@ -793,7 +793,7 @@ export function HyperliquidWalletConnect({
if (!isLoggedIn) {
setError(
language === 'zh'
? '请先登录 NOFX再保存 Agent 钱包用于交易。'
? 'Please sign in before saving the agent wallet for trading.'
: 'Please sign in before saving the agent wallet for trading.'
)
return
@@ -899,7 +899,7 @@ export function HyperliquidWalletConnect({
onClick={() => setOpen((value) => !value)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-bold transition-all border ${
complete
? 'bg-emerald-500/10 border-emerald-400/30 text-emerald-300'
? 'bg-nofx-success/10 border-nofx-success/30 text-nofx-success'
: 'bg-nofx-gold/10 border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/20'
}`}
>
@@ -913,11 +913,11 @@ export function HyperliquidWalletConnect({
{(open || inline) && (
<div
className={`${inline ? 'relative w-full' : 'absolute right-0 top-full mt-2 w-[420px] shadow-2xl shadow-black/50'} rounded-2xl border border-nofx-gold/20 bg-[#11151B] z-[80] overflow-hidden`}
className={`${inline ? 'relative w-full' : 'absolute right-0 top-full mt-2 w-[420px] shadow-2xl shadow-black/10'} rounded-2xl border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter z-[80] overflow-hidden`}
>
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center justify-between p-4 border-b border-[rgba(26,24,19,0.14)]">
<div>
<div className="font-bold text-white">{text.title}</div>
<div className="font-bold text-nofx-text">{text.title}</div>
<div className="text-xs text-nofx-text-muted mt-1">
{text.noCustody}
</div>
@@ -929,7 +929,7 @@ export function HyperliquidWalletConnect({
<button
type="button"
onClick={() => setOpen(false)}
className="p-1 rounded hover:bg-white/10 text-zinc-500"
className="p-1 rounded hover:bg-[rgba(26,24,19,0.06)] text-nofx-text-muted"
>
<X className="w-4 h-4" />
</button>
@@ -943,10 +943,10 @@ export function HyperliquidWalletConnect({
<div
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
step.status === 'done'
? 'bg-emerald-400 text-black'
? 'bg-nofx-success text-white'
: step.status === 'active'
? 'bg-nofx-gold text-black'
: 'bg-zinc-800 text-zinc-500'
? 'bg-nofx-gold text-white'
: 'bg-nofx-bg-deeper text-nofx-text-muted'
}`}
>
{step.status === 'done' ? (
@@ -958,8 +958,8 @@ export function HyperliquidWalletConnect({
<span
className={
step.status === 'pending'
? 'text-zinc-500'
: 'text-zinc-200'
? 'text-nofx-text-muted'
: 'text-nofx-text'
}
>
{step.label}
@@ -969,14 +969,14 @@ export function HyperliquidWalletConnect({
</div>
{error && (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-xs text-red-300">
<div className="rounded-lg border border-nofx-danger/30 bg-nofx-danger/10 p-3 text-xs text-nofx-danger">
{error}
</div>
)}
{!state.mainWallet && !hasWalletProvider && (
<div className="rounded-xl border border-nofx-gold/20 bg-nofx-gold/5 p-3">
<div className="text-sm font-semibold text-zinc-100">
<div className="text-sm font-semibold text-nofx-text">
{text.noWalletTitle}
</div>
<p className="mt-1 text-xs leading-5 text-nofx-text-muted">
@@ -987,7 +987,7 @@ export function HyperliquidWalletConnect({
href="https://rabby.io/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-zinc-200 hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center gap-2 rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text hover:border-[rgba(26,24,19,0.24)] hover:bg-nofx-bg"
>
<Download className="h-3.5 w-3.5" />
{text.installRabby}
@@ -996,7 +996,7 @@ export function HyperliquidWalletConnect({
href="https://metamask.io/download/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-zinc-200 hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center gap-2 rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text hover:border-[rgba(26,24,19,0.24)] hover:bg-nofx-bg"
>
<ExternalLink className="h-3.5 w-3.5" />
{text.installMetaMask}
@@ -1005,14 +1005,14 @@ export function HyperliquidWalletConnect({
</div>
)}
<div className="rounded-xl border border-white/10 bg-black/25 p-3 space-y-2 text-xs">
<div className="rounded-xl border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper p-3 space-y-2 text-xs">
{state.mainWallet && (
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Main</span>
<span className="text-nofx-text-muted">Main</span>
<button
type="button"
onClick={() => copy(state.mainWallet!, 'Main wallet')}
className="font-mono text-zinc-200 hover:text-nofx-gold flex items-center gap-1"
className="font-mono text-nofx-text hover:text-nofx-gold flex items-center gap-1"
>
{shortAddress(state.mainWallet)}{' '}
<Copy className="w-3 h-3" />
@@ -1021,11 +1021,11 @@ export function HyperliquidWalletConnect({
)}
{state.agentAddress && (
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Agent</span>
<span className="text-nofx-text-muted">Agent</span>
<button
type="button"
onClick={() => copy(state.agentAddress!, 'Agent wallet')}
className="font-mono text-zinc-200 hover:text-nofx-gold flex items-center gap-1"
className="font-mono text-nofx-text hover:text-nofx-gold flex items-center gap-1"
>
{shortAddress(state.agentAddress)}{' '}
<Copy className="w-3 h-3" />
@@ -1033,16 +1033,16 @@ export function HyperliquidWalletConnect({
</div>
)}
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Network</span>
<span className="font-mono text-zinc-300">
<span className="text-nofx-text-muted">Network</span>
<span className="font-mono text-nofx-text">
Hyperliquid Mainnet
</span>
</div>
{state.mainWallet && (
<div className="flex items-center justify-between gap-3 border-t border-white/10 pt-2">
<span className="text-zinc-500">{text.agentExpiry}</span>
<div className="flex items-center justify-between gap-3 border-t border-[rgba(26,24,19,0.14)] pt-2">
<span className="text-nofx-text-muted">{text.agentExpiry}</span>
{agentInfoLoading && !agentInfo ? (
<span className="font-mono text-zinc-400">Loading</span>
<span className="font-mono text-nofx-text-muted">Loading</span>
) : agentInfo ? (
(() => {
const { dateStr, daysLeft } = formatAgentExpiry(
@@ -1052,10 +1052,10 @@ export function HyperliquidWalletConnect({
const expired = daysLeft < 0
const soon = daysLeft >= 0 && daysLeft <= 14
const tone = expired
? 'text-red-300'
? 'text-nofx-danger'
: soon
? 'text-amber-300'
: 'text-zinc-200'
? 'text-nofx-gold'
: 'text-nofx-text'
return (
<span className={`font-mono text-right ${tone}`}>
{dateStr}
@@ -1066,7 +1066,7 @@ export function HyperliquidWalletConnect({
)
})()
) : (
<span className="font-mono text-zinc-500">
<span className="font-mono text-nofx-text-muted">
{text.agentNoAuth}
</span>
)}
@@ -1098,14 +1098,14 @@ export function HyperliquidWalletConnect({
{state.mainWallet && (
<div className="rounded-xl border border-nofx-gold/20 bg-nofx-gold/5 p-3 space-y-3 text-xs">
<div className="flex items-center justify-between gap-3">
<span className="font-bold text-zinc-100">
<span className="font-bold text-nofx-text">
{text.balance}
</span>
<button
type="button"
onClick={() => void refreshBalance()}
disabled={balanceLoading}
className="flex items-center gap-1 text-zinc-400 hover:text-nofx-gold disabled:opacity-60"
className="flex items-center gap-1 text-nofx-text-muted hover:text-nofx-gold disabled:opacity-60"
>
<RefreshCw
className={`w-3 h-3 ${balanceLoading ? 'animate-spin' : ''}`}
@@ -1114,37 +1114,37 @@ export function HyperliquidWalletConnect({
</button>
</div>
{balanceError ? (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-2 text-red-300">
<div className="rounded-lg border border-nofx-danger/30 bg-nofx-danger/10 p-2 text-nofx-danger">
{balanceError}
</div>
) : (
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.withdrawable}</div>
<div className="mt-1 font-mono text-sm font-bold text-emerald-300">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.withdrawable}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-success">
{balanceLoading && !account
? 'Loading…'
: `${formatUSDC(account?.withdrawable)} USDC`}
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.equity}</div>
<div className="mt-1 font-mono text-sm font-bold text-zinc-100">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.equity}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-text">
{balanceLoading && !account
? 'Loading…'
: `${formatUSDC(account?.accountValue)} USDC`}
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.marginUsed}</div>
<div className="mt-1 font-mono text-sm font-bold text-zinc-100">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.marginUsed}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-text">
{formatUSDC(account?.totalMarginUsed)} USDC
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.unrealizedPnl}</div>
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.unrealizedPnl}</div>
<div
className={`mt-1 font-mono text-sm font-bold ${(account?.unrealizedPnl ?? 0) >= 0 ? 'text-emerald-300' : 'text-red-300'}`}
className={`mt-1 font-mono text-sm font-bold ${(account?.unrealizedPnl ?? 0) >= 0 ? 'text-nofx-success' : 'text-nofx-danger'}`}
>
{formatSignedUSDC(account?.unrealizedPnl)} USDC
</div>
@@ -1192,7 +1192,7 @@ export function HyperliquidWalletConnect({
)}
{complete && (
<>
<div className="rounded-lg border border-emerald-400/30 bg-emerald-500/10 p-3 text-sm text-emerald-200 flex items-center gap-2">
<div className="rounded-lg border border-nofx-success/30 bg-nofx-success/10 p-3 text-sm text-nofx-success flex items-center gap-2">
<Shield className="w-4 h-4" /> {text.done}
</div>
<button
@@ -1201,26 +1201,26 @@ export function HyperliquidWalletConnect({
className="w-full flex items-center justify-center gap-2 rounded-xl border border-nofx-gold/30 bg-nofx-gold/10 px-4 py-3 text-sm font-bold text-nofx-gold transition hover:bg-nofx-gold/20"
>
{language === 'zh'
? '重新授权交易'
? 'Re-authorize trading'
: 'Re-authorize trading'}
</button>
</>
)}
</div>
<div className="flex items-center justify-between pt-2 border-t border-white/10">
<div className="flex items-center justify-between pt-2 border-t border-[rgba(26,24,19,0.14)]">
<a
href="https://app.hyperliquid.xyz/"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-zinc-500 hover:text-nofx-gold flex items-center gap-1"
className="text-xs text-nofx-text-muted hover:text-nofx-gold flex items-center gap-1"
>
Open Hyperliquid <ExternalLink className="w-3 h-3" />
</a>
<button
type="button"
onClick={resetFlow}
className="text-xs text-zinc-500 hover:text-red-300"
className="text-xs text-nofx-text-muted hover:text-nofx-danger"
>
Reset
</button>
@@ -1246,7 +1246,7 @@ function ActionButton({
type="button"
disabled={busy}
onClick={onClick}
className="w-full flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-4 py-3 text-sm font-bold text-black transition hover:bg-yellow-400 disabled:opacity-60 disabled:cursor-not-allowed"
className="w-full flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-4 py-3 text-sm font-bold text-white transition hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed"
>
{busy ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
{label}

View File

@@ -3,7 +3,7 @@ import { useLanguage } from '../../contexts/LanguageContext'
import type { Language } from '../../i18n/translations'
const languages: { code: Language; label: string }[] = [
{ code: 'zh', label: '中文' },
{ code: 'zh', label: 'Chinese' },
{ code: 'en', label: 'EN' },
{ code: 'id', label: 'ID' },
]
@@ -12,8 +12,8 @@ export function LanguageSwitcher() {
const { language, setLanguage } = useLanguage()
return (
<div className="absolute top-4 right-4 z-50 flex items-center gap-1 rounded-lg p-1 border border-white/10 bg-white/5 backdrop-blur-sm">
<Globe size={14} className="text-zinc-500 ml-1.5 mr-0.5" />
<div className="absolute top-4 right-4 z-50 flex items-center gap-1 rounded-lg p-1 border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter backdrop-blur-sm">
<Globe size={14} className="text-nofx-text-muted ml-1.5 mr-0.5" />
{languages.map(({ code, label }) => (
<button
key={code}
@@ -22,7 +22,7 @@ export function LanguageSwitcher() {
className={`px-2.5 py-1 rounded text-xs font-semibold transition-all ${
language === code
? 'bg-nofx-gold/15 text-nofx-gold'
: 'text-zinc-500 hover:text-zinc-300 bg-transparent'
: 'text-nofx-text-muted hover:text-nofx-text bg-transparent'
}`}
>
{label}

View File

@@ -19,122 +19,122 @@ export const METRIC_DEFINITIONS: Record<string, MetricDefinition> = {
total_return: {
key: 'total_return',
nameEn: 'Total Return',
nameZh: '总收益率',
nameZh: 'Total Return',
formula: 'R_{total} = \\frac{V_{end} - V_{start}}{V_{start}} \\times 100\\%',
descriptionEn: 'Measures overall portfolio performance from start to end',
descriptionZh: '衡量投资组合从开始到结束的整体收益表现',
descriptionZh: 'Measures overall portfolio performance from start to end',
},
annualized_return: {
key: 'annualized_return',
nameEn: 'Annualized Return',
nameZh: '年化收益率',
nameZh: 'Annualized Return',
formula: 'R_{ann} = \\left(1 + R_{total}\\right)^{\\frac{252}{n}} - 1',
descriptionEn: 'Standardized yearly return rate (252 trading days)',
descriptionZh: '标准化年度收益率按252个交易日计算',
descriptionZh: 'Standardized yearly return rate (252 trading days)',
},
max_drawdown: {
key: 'max_drawdown',
nameEn: 'Maximum Drawdown',
nameZh: '最大回撤',
nameZh: 'Maximum Drawdown',
formula: 'MDD = \\max_{t} \\left( \\frac{Peak_t - Trough_t}{Peak_t} \\right)',
descriptionEn: 'Largest peak-to-trough decline during the period',
descriptionZh: '期间内从峰值到谷底的最大跌幅',
descriptionZh: 'Largest peak-to-trough decline during the period',
},
sharpe_ratio: {
key: 'sharpe_ratio',
nameEn: 'Sharpe Ratio',
nameZh: '夏普比率',
nameZh: 'Sharpe Ratio',
formula: 'SR = \\frac{\\bar{r} - r_f}{\\sigma}',
descriptionEn: 'Risk-adjusted return per unit of volatility (r̄=avg return, rf=risk-free rate, σ=std dev)',
descriptionZh: '单位波动风险下的超额收益r̄=平均收益rf=无风险利率,σ=标准差)',
descriptionZh: 'Risk-adjusted return per unit of volatility (r̄=avg return, rf=risk-free rate, σ=std dev)',
},
sortino_ratio: {
key: 'sortino_ratio',
nameEn: 'Sortino Ratio',
nameZh: '索提诺比率',
nameZh: 'Sortino Ratio',
formula: 'Sortino = \\frac{\\bar{r} - r_f}{\\sigma_d}',
descriptionEn: 'Return per unit of downside risk (σd=downside deviation)',
descriptionZh: '单位下行风险的收益σd=下行标准差)',
descriptionZh: 'Return per unit of downside risk (σd=downside deviation)',
},
calmar_ratio: {
key: 'calmar_ratio',
nameEn: 'Calmar Ratio',
nameZh: '卡玛比率',
nameZh: 'Calmar Ratio',
formula: 'Calmar = \\frac{R_{ann}}{|MDD|}',
descriptionEn: 'Annualized return divided by maximum drawdown',
descriptionZh: '年化收益率与最大回撤的比值',
descriptionZh: 'Annualized return divided by maximum drawdown',
},
win_rate: {
key: 'win_rate',
nameEn: 'Win Rate',
nameZh: '胜率',
nameZh: 'Win Rate',
formula: 'WinRate = \\frac{N_{win}}{N_{total}} \\times 100\\%',
descriptionEn: 'Percentage of profitable trades',
descriptionZh: '盈利交易占总交易数的百分比',
descriptionZh: 'Percentage of profitable trades',
},
profit_factor: {
key: 'profit_factor',
nameEn: 'Profit Factor',
nameZh: '盈亏比',
nameZh: 'Profit Factor',
formula: 'PF = \\frac{\\sum Profits}{|\\sum Losses|}',
descriptionEn: 'Ratio of gross profit to gross loss',
descriptionZh: '总盈利与总亏损的比值',
descriptionZh: 'Ratio of gross profit to gross loss',
},
volatility: {
key: 'volatility',
nameEn: 'Volatility',
nameZh: '波动率',
nameZh: 'Volatility',
formula: '\\sigma = \\sqrt{\\frac{1}{n}\\sum_{i=1}^{n}(r_i - \\bar{r})^2}',
descriptionEn: 'Standard deviation of returns',
descriptionZh: '收益率的标准差',
descriptionZh: 'Standard deviation of returns',
},
var_95: {
key: 'var_95',
nameEn: 'VaR (95%)',
nameZh: '风险价值',
nameZh: 'VaR (95%)',
formula: 'P(R < VaR_{95\\%}) = 5\\%',
descriptionEn: '95% confidence level maximum expected loss',
descriptionZh: '95%置信水平下的最大预期损失',
descriptionZh: '95% confidence level maximum expected loss',
},
alpha: {
key: 'alpha',
nameEn: 'Alpha',
nameZh: '超额收益',
nameZh: 'Alpha',
formula: '\\alpha = R_{portfolio} - R_{benchmark}',
descriptionEn: 'Excess return over benchmark',
descriptionZh: '相对于基准的超额收益',
descriptionZh: 'Excess return over benchmark',
},
beta: {
key: 'beta',
nameEn: 'Beta',
nameZh: '贝塔系数',
nameZh: 'Beta',
formula: '\\beta = \\frac{Cov(R_p, R_m)}{Var(R_m)}',
descriptionEn: 'Portfolio sensitivity to market movements',
descriptionZh: '投资组合对市场波动的敏感度',
descriptionZh: 'Portfolio sensitivity to market movements',
},
information_ratio: {
key: 'information_ratio',
nameEn: 'Information Ratio',
nameZh: '信息比率',
nameZh: 'Information Ratio',
formula: 'IR = \\frac{\\alpha}{\\sigma_{tracking}}',
descriptionEn: 'Alpha per unit of tracking error',
descriptionZh: '单位跟踪误差的超额收益',
descriptionZh: 'Alpha per unit of tracking error',
},
avg_trade_pnl: {
key: 'avg_trade_pnl',
nameEn: 'Avg Trade PnL',
nameZh: '平均盈亏',
nameZh: 'Avg Trade PnL',
formula: '\\bar{PnL} = \\frac{\\sum PnL_i}{N}',
descriptionEn: 'Average profit/loss per trade',
descriptionZh: '每笔交易的平均盈亏',
descriptionZh: 'Average profit/loss per trade',
},
expectancy: {
key: 'expectancy',
nameEn: 'Expectancy',
nameZh: '期望收益',
nameZh: 'Expectancy',
formula: 'E = (WinRate \\times \\bar{W}) - (LossRate \\times \\bar{L})',
descriptionEn: 'Expected return per trade',
descriptionZh: '每笔交易的期望收益',
descriptionZh: 'Expected return per trade',
},
}
@@ -259,11 +259,11 @@ export function MetricTooltip({
>
<div
style={{
background: 'linear-gradient(145deg, #1E2329 0%, #2B3139 100%)',
border: '1px solid #3B4149',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
borderRadius: '12px',
padding: '16px',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.8)',
boxShadow: '0 25px 50px -12px rgba(26, 24, 19, 0.18)',
}}
>
{/* Header */}
@@ -273,27 +273,27 @@ export function MetricTooltip({
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #3B4149'
borderBottom: '1px solid rgba(26,24,19,0.14)'
}}>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: '#F0B90B'
background: '#E0483B'
}} />
<span style={{ fontWeight: 'bold', fontSize: '14px', color: '#EAECEF' }}>
<span style={{ fontWeight: 'bold', fontSize: '14px', color: '#1A1813' }}>
{name}
</span>
</div>
{/* Formula */}
<div style={{
background: 'rgba(0,0,0,0.3)',
background: '#E8E2D5',
borderRadius: '8px',
padding: '12px',
marginBottom: '12px'
}}>
<div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}>
<div style={{ fontSize: '12px', color: '#8A8478', marginBottom: '8px' }}>
{formulaLabel}
</div>
<div style={{
@@ -301,7 +301,7 @@ export function MetricTooltip({
justifyContent: 'center',
alignItems: 'center',
padding: '8px 4px',
color: '#EAECEF',
color: '#1A1813',
overflowX: 'auto',
overflowY: 'hidden',
maxWidth: '100%',
@@ -312,7 +312,7 @@ export function MetricTooltip({
</div>
{/* Description */}
<p style={{ fontSize: '12px', lineHeight: '1.5', color: '#B7BDC6', margin: 0 }}>
<p style={{ fontSize: '12px', lineHeight: '1.5', color: '#8A8478', margin: 0 }}>
{description}
</p>
</div>
@@ -333,8 +333,8 @@ export function MetricTooltip({
}
setShow(!show)
}}
className={`p-0.5 rounded-full transition-colors hover:bg-white/10 ${className}`}
style={{ color: '#848E9C' }}
className={`p-0.5 rounded-full transition-colors hover:bg-[rgba(26,24,19,0.06)] ${className}`}
style={{ color: '#8A8478' }}
aria-label={`Info about ${name}`}
>
<HelpCircle size={size} />

View File

@@ -17,9 +17,9 @@ const MODEL_COLORS: Record<string, string> = {
claw402: '#7C3AED',
}
// 获取AI模型图标的函数
// Returns the icon for an AI model
export const getModelIcon = (modelType: string, props: IconProps = {}) => {
// 支持完整ID或类型名
// Supports full ID or type name
const type = modelType.includes('_') ? modelType.split('_').pop() : modelType
let iconPath: string | null = null
@@ -67,8 +67,8 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => {
)
}
// 获取模型颜色用于没有图标时的fallback
// Returns the model color (fallback for when there is no icon)
export const getModelColor = (modelType: string): string => {
const type = modelType.includes('_') ? modelType.split('_').pop() : modelType
return MODEL_COLORS[type || ''] || '#60a5fa'
return MODEL_COLORS[type || ''] || '#E0483B'
}

View File

@@ -9,11 +9,11 @@ export function SiteFooter({ language }: SiteFooterProps) {
return (
<footer
className="mt-16"
style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}
style={{ borderTop: '1px solid rgba(26,24,19,0.14)', background: '#F7F4EC' }}
>
<div
className="max-w-[1920px] mx-auto px-6 py-6 text-center text-sm"
style={{ color: '#5E6673' }}
style={{ color: '#8A8478' }}
>
<p>{t('footerTitle', language)}</p>
<p className="mt-1">{t('footerWarning', language)}</p>
@@ -24,19 +24,19 @@ export function SiteFooter({ language }: SiteFooterProps) {
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
background: '#F1ECE2',
color: '#8A8478',
border: '1px solid rgba(26,24,19,0.14)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.borderColor = '#F0B90B'
e.currentTarget.style.background = '#E8E2D5'
e.currentTarget.style.color = '#1A1813'
e.currentTarget.style.borderColor = '#E0483B'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
e.currentTarget.style.background = '#F1ECE2'
e.currentTarget.style.color = '#8A8478'
e.currentTarget.style.borderColor = 'rgba(26,24,19,0.14)'
}}
>
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
@@ -50,19 +50,19 @@ export function SiteFooter({ language }: SiteFooterProps) {
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
background: '#F1ECE2',
color: '#8A8478',
border: '1px solid rgba(26,24,19,0.14)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.background = '#E8E2D5'
e.currentTarget.style.color = '#1A1813'
e.currentTarget.style.borderColor = '#1DA1F2'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
e.currentTarget.style.background = '#F1ECE2'
e.currentTarget.style.color = '#8A8478'
e.currentTarget.style.borderColor = 'rgba(26,24,19,0.14)'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
@@ -76,19 +76,19 @@ export function SiteFooter({ language }: SiteFooterProps) {
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{
background: '#1E2329',
color: '#848E9C',
border: '1px solid #2B3139',
background: '#F1ECE2',
color: '#8A8478',
border: '1px solid rgba(26,24,19,0.14)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139'
e.currentTarget.style.color = '#EAECEF'
e.currentTarget.style.background = '#E8E2D5'
e.currentTarget.style.color = '#1A1813'
e.currentTarget.style.borderColor = '#0088cc'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329'
e.currentTarget.style.color = '#848E9C'
e.currentTarget.style.borderColor = '#2B3139'
e.currentTarget.style.background = '#F1ECE2'
e.currentTarget.style.color = '#8A8478'
e.currentTarget.style.borderColor = 'rgba(26,24,19,0.14)'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">

View File

@@ -73,15 +73,15 @@ export function WebCryptoEnvironmentCheck({
const isCompact = variant === 'compact'
const containerClass = isCompact
? 'p-3 rounded border border-gray-700 bg-gray-900 space-y-3'
: 'p-4 rounded border border-[#2B3139] bg-[#0B0E11] space-y-4'
? 'p-3 rounded border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter space-y-3'
: 'p-4 rounded border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter space-y-4'
const descriptionColor = isCompact ? '#CBD5F5' : '#A1AEC8'
const descriptionColor = isCompact ? '#8A8478' : '#8A8478'
const showInfo = status !== 'idle'
const statusRendererMap: Record<WebCryptoCheckStatus, () => ReactNode> = {
secure: () => (
<div className="flex items-start gap-2 text-green-400 text-xs">
<div className="flex items-start gap-2 text-[#2E8B57] text-xs">
<ShieldCheck className="w-4 h-4 flex-shrink-0" />
<div>
<div className="font-semibold">
@@ -111,7 +111,7 @@ export function WebCryptoEnvironmentCheck({
</div>
),
unsupported: () => (
<div className="text-xs" style={{ color: '#F87171' }}>
<div className="text-xs" style={{ color: '#D6433A' }}>
<div className="flex items-start gap-2 mb-1">
<ShieldAlert className="w-4 h-4 flex-shrink-0" />
<div className="font-semibold">
@@ -122,7 +122,7 @@ export function WebCryptoEnvironmentCheck({
</div>
),
disabled: () => (
<div className="flex items-start gap-2 text-gray-400 text-xs">
<div className="flex items-start gap-2 text-nofx-text-muted text-xs">
<ShieldMinus className="w-4 h-4 flex-shrink-0" />
<div>
<div className="font-semibold">
@@ -135,7 +135,7 @@ export function WebCryptoEnvironmentCheck({
checking: () => (
<div
className="flex items-center gap-2 text-xs"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<Loader2 className="w-4 h-4 animate-spin" />
<span>{t('environmentCheck.checking', language)}</span>

View File

@@ -19,11 +19,9 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
}
return (
<div className="min-h-screen bg-nofx-bg-deeper text-white font-mono relative overflow-hidden flex items-center justify-center px-4">
{/* Background Grid & Scanlines */}
<div className="fixed inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"></div>
<div className="fixed inset-0 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
<div className="fixed inset-0 pointer-events-none opacity-[0.03] bg-[linear-gradient(transparent_50%,rgba(0,0,0,0.5)_50%)] bg-[length:100%_4px]"></div>
<div className="min-h-screen bg-nofx-bg-deeper text-nofx-text font-mono relative overflow-hidden flex items-center justify-center px-4">
{/* Background Grid */}
<div className="fixed inset-0 bg-[linear-gradient(to_right,rgba(26,24,19,0.04)_1px,transparent_1px),linear-gradient(to_bottom,rgba(26,24,19,0.04)_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"></div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
@@ -31,15 +29,15 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
transition={{ duration: 0.5 }}
className="max-w-lg w-full relative z-10"
>
<div className="bg-zinc-900/40 backdrop-blur-md border border-red-500/30 rounded-lg overflow-hidden relative group">
<div className="bg-nofx-bg-lighter border border-[#D6433A]/30 rounded-lg overflow-hidden relative group">
{/* Top Bar */}
<div className="flex items-center justify-between px-4 py-2 bg-red-900/20 border-b border-red-500/30">
<div className="flex items-center justify-between px-4 py-2 bg-[#D6433A]/10 border-b border-[#D6433A]/30">
<div className="flex gap-1.5 opacity-50">
<div className="w-2.5 h-2.5 rounded-full bg-red-500"></div>
<div className="w-2.5 h-2.5 rounded-full bg-zinc-600"></div>
<div className="w-2.5 h-2.5 rounded-full bg-zinc-600"></div>
<div className="w-2.5 h-2.5 rounded-full bg-[#D6433A]"></div>
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(26,24,19,0.25)]"></div>
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(26,24,19,0.25)]"></div>
</div>
<div className="text-[10px] text-red-400 font-mono tracking-widest animate-pulse">
<div className="text-[10px] text-[#D6433A] font-mono tracking-widest animate-pulse">
ACCESS_DENIED // ERROR_403
</div>
</div>
@@ -47,22 +45,21 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
<div className="p-8 text-center">
{/* Icon */}
<div className="relative mx-auto mb-8 w-20 h-20 flex items-center justify-center">
<div className="absolute inset-0 bg-red-500/20 rounded-full animate-ping opacity-50"></div>
<div className="relative z-10 p-4 border-2 border-red-500/50 rounded-full bg-black/50">
<ShieldAlert className="w-8 h-8 text-red-500" />
<div className="relative z-10 p-4 border-2 border-[#D6433A]/50 rounded-full bg-nofx-bg-deeper">
<ShieldAlert className="w-8 h-8 text-[#D6433A]" />
</div>
</div>
{/* Title */}
<h1 className="text-2xl font-bold mb-2 tracking-widest text-white uppercase glitch-text">
<span className="text-red-500">RESTRICTED</span> ACCESS
<h1 className="text-2xl font-bold mb-2 tracking-widest text-nofx-text uppercase">
<span className="text-[#D6433A]">RESTRICTED</span> ACCESS
</h1>
<div className="h-[1px] w-full bg-gradient-to-r from-transparent via-red-900/50 to-transparent my-4"></div>
<div className="h-[1px] w-full bg-gradient-to-r from-transparent via-[#D6433A]/40 to-transparent my-4"></div>
{/* Description */}
<p className="text-xs text-zinc-400 mb-8 leading-relaxed font-mono px-4">
<span className="text-red-400">[SYSTEM_MESSAGE]:</span> YOUR
<p className="text-xs text-nofx-text-muted mb-8 leading-relaxed font-mono px-4">
<span className="text-[#D6433A]">[SYSTEM_MESSAGE]:</span> YOUR
IDENTIFIER IS NOT ON THE ACTIVE WHITELIST.
<br />
<br />
@@ -72,14 +69,14 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
</p>
{/* Info Box */}
<div className="bg-red-950/20 border border-red-900/30 p-4 rounded mb-8 text-left">
<div className="bg-[#D6433A]/8 border border-[#D6433A]/25 p-4 rounded mb-8 text-left">
<div className="flex items-start gap-3">
<Lock className="w-4 h-4 text-red-500 mt-0.5" />
<Lock className="w-4 h-4 text-[#D6433A] mt-0.5" />
<div>
<h3 className="text-xs font-bold text-red-400 uppercase mb-1">
<h3 className="text-xs font-bold text-[#D6433A] uppercase mb-1">
Authorization Protocol
</h3>
<p className="text-[10px] text-zinc-500 leading-tight">
<p className="text-[10px] text-nofx-text-muted leading-tight">
Access is rolled out in batches. If you believe this is an
error, please verify your credentials or contact system
administrators.
@@ -92,7 +89,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
<div className="space-y-3">
<button
onClick={handleBackToLogin}
className="w-full flex items-center justify-center gap-2 py-3 border border-zinc-700 bg-black hover:bg-zinc-900 hover:border-red-500 hover:text-red-500 text-zinc-400 transition-all text-xs font-bold tracking-widest uppercase group"
className="w-full flex items-center justify-center gap-2 py-3 border border-[rgba(26,24,19,0.14)] bg-nofx-bg hover:bg-nofx-bg-deeper hover:border-[#D6433A] hover:text-[#D6433A] text-nofx-text-muted transition-all text-xs font-bold tracking-widest uppercase group"
>
<ArrowLeft className="w-3 h-3 group-hover:-translate-x-1 transition-transform" />
RETURN TO LOGIN
@@ -103,7 +100,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
href={OFFICIAL_LINKS.twitter}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 py-2 border border-zinc-800 bg-zinc-900/50 hover:bg-zinc-800 text-zinc-500 hover:text-white transition-colors text-[10px] uppercase"
className="flex items-center justify-center gap-2 py-2 border border-[rgba(26,24,19,0.14)] bg-nofx-bg hover:bg-nofx-bg-deeper text-nofx-text-muted hover:text-nofx-text transition-colors text-[10px] uppercase"
>
<Twitter className="w-3 h-3" />
Updates
@@ -112,7 +109,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
href={OFFICIAL_LINKS.telegram}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 py-2 border border-zinc-800 bg-zinc-900/50 hover:bg-zinc-800 text-zinc-500 hover:text-white transition-colors text-[10px] uppercase"
className="flex items-center justify-center gap-2 py-2 border border-[rgba(26,24,19,0.14)] bg-nofx-bg hover:bg-nofx-bg-deeper text-nofx-text-muted hover:text-nofx-text transition-colors text-[10px] uppercase"
>
<Send className="w-3 h-3" />
Support
@@ -122,7 +119,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
</div>
{/* Footer */}
<div className="bg-black/80 p-2 text-[9px] text-zinc-700 text-center border-t border-zinc-800 font-mono uppercase">
<div className="bg-nofx-bg-deeper p-2 text-[9px] text-nofx-text-muted text-center border-t border-[rgba(26,24,19,0.14)] font-mono uppercase">
ERR_CODE: WLIST_0x403 // SECURITY_LAYER_ACTIVE
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react'
import { t, type Language } from '../../i18n/translations'
import type { FAQCategory } from '../../data/faqData'
// RoadmapWidget 移除动态嵌入,按需仅展示外部链接
// RoadmapWidget: removed dynamic embedding; only show external links on demand
interface FAQContentProps {
categories: FAQCategory[]
@@ -56,11 +56,11 @@ export function FAQContent({
return (
<div className="space-y-12">
{categories.map((category) => (
<div key={category.id} className="nofx-glass p-8 rounded-xl border border-white/5">
<div key={category.id} className="nofx-glass p-8 rounded-xl border border-[rgba(26,24,19,0.14)]">
{/* Category Header */}
<div className="flex items-center gap-3 mb-6 pb-3 border-b border-white/10">
<div className="flex items-center gap-3 mb-6 pb-3 border-b border-[rgba(26,24,19,0.14)]">
<category.icon className="w-7 h-7 text-nofx-gold" />
<h2 className="text-2xl font-bold text-nofx-text-main">
<h2 className="text-2xl font-bold text-nofx-text">
{t(category.titleKey, language)}
</h2>
</div>
@@ -76,7 +76,7 @@ export function FAQContent({
className="scroll-mt-24"
>
{/* Question */}
<h3 className="text-xl font-semibold mb-3 text-nofx-text-main">
<h3 className="text-xl font-semibold mb-3 text-nofx-text">
{t(item.questionKey, language)}
</h3>
@@ -85,41 +85,42 @@ export function FAQContent({
{item.id === 'github-projects-tasks' ? (
<div className="space-y-3">
<div className="text-base">
{language === 'zh' ? '链接:' : 'Links:'}{' '}
{language === 'zh' ? 'Links:' : 'Links:'}{' '}
<a
href="https://github.com/orgs/NoFxAiOS/projects/3"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
{language === 'zh' ? '路线图' : 'Roadmap'}
{language === 'zh' ? 'Roadmap' : 'Roadmap'}
</a>
{' | '}
<a
href="https://github.com/orgs/NoFxAiOS/projects/5"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
{language === 'zh' ? '任务看板' : 'Task Dashboard'}
{language === 'zh' ? 'Task Dashboard' : 'Task Dashboard'}
</a>
</div>
<ol className="list-decimal pl-5 space-y-1 text-base">
{language === 'zh' ? (
<>
<li>
good first issue / help
wanted / frontend / backend
Open the links above and filter by labels (good
first issue / help wanted / frontend / backend).
</li>
<li>
Acceptance
Criteria
Open the task and read the Description &
Acceptance Criteria.
</li>
<li>assign me</li>
<li>Fork GitHub </li>
<li>
fork <code>dev</code>{' '}
Comment "assign me" or self-assign (if permitted).
</li>
<li>Fork the repository to your GitHub account.</li>
<li>
Sync your fork's <code>dev</code> with upstream:
<code className="ml-2">
git remote add upstream
https://github.com/NoFxAiOS/nofx.git
@@ -134,28 +135,28 @@ export function FAQContent({
<code>git push origin dev</code>
</li>
<li>
fork <code>dev</code>
Create a feature branch from your fork's{' '}
<code>dev</code>:
<code className="ml-2">
git checkout -b feat/your-topic
</code>
</li>
<li>
fork
Push to your fork:
<code className="ml-2">
git push origin feat/your-topic
</code>
</li>
<li>
PRbase <code>NoFxAiOS/nofx:dev</code>{' '}
compare {' '}
<code>/nofx:feat/your-topic</code>
Open a PR: base <code>NoFxAiOS/nofx:dev</code>
compare{' '}
<code>your-username/nofx:feat/your-topic</code>.
</li>
<li>
PR Issue
<code className="ml-1">Closes #123</code>
PR {' '}
<code>upstream/dev</code>{' '}
rebase
In PR, reference the Issue (e.g.,{' '}
<code className="ml-1">Closes #123</code>) and
choose the proper PR template; rebase onto{' '}
<code>upstream/dev</code> as needed.
</li>
</>
) : (
@@ -218,38 +219,13 @@ export function FAQContent({
<div
className="rounded p-3 mt-3"
style={{
background: 'rgba(240, 185, 11, 0.08)',
border: '1px solid rgba(240, 185, 11, 0.25)',
background: 'rgba(224, 72, 59, 0.08)',
border: '1px solid rgba(224, 72, 59, 0.25)',
}}
>
{language === 'zh' ? (
<div className="text-sm">
<strong style={{ color: '#F0B90B' }}></strong>{' '}
Bounty/
Review/
<a
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
>
bounty
</a>
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
>
Bounty Claim
</a>
</div>
) : (
<div className="text-sm">
<strong style={{ color: '#F0B90B' }}>Note:</strong>{' '}
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
Contribution incentives are available (e.g., cash
bounties, badges & shout-outs, priority
review/merge, beta access). Prefer tasks with
@@ -257,7 +233,7 @@ export function FAQContent({
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
bounty label
</a>
@@ -266,7 +242,32 @@ export function FAQContent({
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
Bounty Claim
</a>
after completion.
</div>
) : (
<div className="text-sm">
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
Contribution incentives are available (e.g., cash
bounties, badges & shout-outs, priority
review/merge, beta access). Prefer tasks with
<a
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#E0483B' }}
>
bounty label
</a>
, or file a
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#E0483B' }}
>
Bounty Claim
</a>
@@ -278,7 +279,7 @@ export function FAQContent({
) : item.id === 'contribute-pr-guidelines' ? (
<div className="space-y-3">
<div className="text-base">
{language === 'zh' ? '参考文档:' : 'References:'}{' '}
{language === 'zh' ? 'References:' : 'References:'}{' '}
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
target="_blank"
@@ -301,35 +302,37 @@ export function FAQContent({
{language === 'zh' ? (
<>
<li>
Fork fork <code>dev</code>{' '}
<code>main</code>{' '}
After forking, branch from your fork's{' '}
<code>dev</code>; avoid direct commits to upstream{' '}
<code>main</code>.
</li>
<li>
feat/fix/docs/
Conventional Commits
Branch naming: feat/…, fix/…, docs/…; commit
messages follow Conventional Commits.
</li>
<li>
Run checks before PR:
<code className="ml-2">
npm --prefix web run lint && npm --prefix web
run build
</code>
</li>
<li> UI </li>
<li>
PR
frontend/backend/docs/general
For UI changes, attach screenshots or a short
video.
</li>
<li>
PR Issue
<code className="ml-1">Closes #123</code>PR
<code>NoFxAiOS/nofx:dev</code>
Choose the proper PR template
(frontend/backend/docs/general).
</li>
<li>
<code>upstream/dev</code>{' '}
rebase CI PR
Link the Issue in PR (e.g.,{' '}
<code className="ml-1">Closes #123</code>) and
target <code>NoFxAiOS/nofx:dev</code>.
</li>
<li>
Keep rebasing onto <code>upstream/dev</code>,
ensure CI passes; prefer small and focused PRs.
</li>
</>
) : (
@@ -375,30 +378,6 @@ export function FAQContent({
{language === 'zh' ? (
<div className="text-sm">
<strong className="text-nofx-gold">Note:</strong>{' '}
我们为高质量贡献提供激励Bounty/奖金、荣誉徽章与鸣谢、优先
Review/合并与内测资格 等)。 详情可关注带
<a
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
>
bounty 标签
</a>
的任务,或使用
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
>
Bounty Claim 模板
</a>
提交申请。
</div>
) : (
<div className="text-sm">
<strong style={{ color: '#F0B90B' }}>Note:</strong>{' '}
We offer contribution incentives (bounties, badges,
shout-outs, priority review/merge, beta access).
Look for tasks with
@@ -406,7 +385,7 @@ export function FAQContent({
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
bounty label
</a>
@@ -415,7 +394,32 @@ export function FAQContent({
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
Bounty Claim template
</a>
when ready.
</div>
) : (
<div className="text-sm">
<strong style={{ color: '#E0483B' }}>Note:</strong>{' '}
We offer contribution incentives (bounties, badges,
shout-outs, priority review/merge, beta access).
Look for tasks with
<a
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
target="_blank"
rel="noreferrer"
style={{ color: '#E0483B' }}
>
bounty label
</a>
, or submit a
<a
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
target="_blank"
rel="noreferrer"
style={{ color: '#E0483B' }}
>
Bounty Claim
</a>
@@ -430,7 +434,7 @@ export function FAQContent({
</div>
{/* Divider */}
<div className="mt-6 h-px bg-white/5" />
<div className="mt-6 h-px bg-[rgba(26,24,19,0.14)]" />
</section>
))}
</div>

View File

@@ -63,11 +63,11 @@ export function FAQLayout({ language }: FAQLayoutProps) {
{/* Page Header */}
<div className="text-center mb-12">
<div className="flex items-center justify-center gap-3 mb-4">
<div className="w-16 h-16 rounded-full flex items-center justify-center bg-gradient-to-br from-nofx-gold to-[#FCD535] shadow-[0_8px_24px_rgba(240,185,11,0.4)]">
<HelpCircle className="w-8 h-8 text-[#0B0E11]" />
<div className="w-16 h-16 rounded-full flex items-center justify-center bg-nofx-gold shadow-lg">
<HelpCircle className="w-8 h-8 text-nofx-bg" />
</div>
</div>
<h1 className="text-4xl font-bold mb-4 text-nofx-text-main">
<h1 className="text-4xl font-bold mb-4 text-nofx-text">
{t('faqTitle', language)}
</h1>
<p className="text-lg mb-8 text-nofx-text-muted">
@@ -80,7 +80,7 @@ export function FAQLayout({ language }: FAQLayoutProps) {
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
placeholder={
language === 'zh' ? '搜索常见问题...' : 'Search FAQ...'
language === 'zh' ? 'Search FAQ...' : 'Search FAQ...'
}
/>
</div>
@@ -108,21 +108,20 @@ export function FAQLayout({ language }: FAQLayoutProps) {
/>
) : (
<div className="text-center py-12">
<p className="text-lg" style={{ color: '#848E9C' }}>
<p className="text-lg" style={{ color: '#8A8478' }}>
{language === 'zh'
? '没有找到匹配的问题'
? 'No matching questions found'
: 'No matching questions found'}
</p>
<button
onClick={() => setSearchTerm('')}
className="mt-4 px-6 py-2 rounded-lg font-semibold transition-all hover:opacity-90"
style={{
background:
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
color: '#0B0E11',
background: '#E0483B',
color: '#F1ECE2',
}}
>
{language === 'zh' ? '清除搜索' : 'Clear Search'}
{language === 'zh' ? 'Clear Search' : 'Clear Search'}
</button>
</div>
)}
@@ -133,15 +132,14 @@ export function FAQLayout({ language }: FAQLayoutProps) {
<div
className="mt-16 p-8 rounded-lg text-center"
style={{
background:
'linear-gradient(135deg, rgba(240, 185, 11, 0.1) 0%, rgba(252, 213, 53, 0.05) 100%)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: 'rgba(224, 72, 59, 0.08)',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
>
<h3 className="text-xl font-bold mb-3" style={{ color: '#EAECEF' }}>
<h3 className="text-xl font-bold mb-3" style={{ color: '#1A1813' }}>
{t('faqStillHaveQuestions', language)}
</h3>
<p className="mb-6" style={{ color: '#848E9C' }}>
<p className="mb-6" style={{ color: '#8A8478' }}>
{t('faqContactUs', language)}
</p>
<div className="flex items-center justify-center gap-4">
@@ -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)}

View File

@@ -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 && (
<button
onClick={() => onSearchChange('')}
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-nofx-text-muted hover:text-white transition-colors"
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
>
<X className="w-5 h-5" />
</button>

View File

@@ -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',
}}
>
<div className="space-y-6">
{categories.map((category) => (
<div key={category.id} className="nofx-glass p-4 rounded-xl border border-white/5">
<div key={category.id} className="nofx-glass p-4 rounded-xl border border-[rgba(26,24,19,0.14)]">
{/* Category Title */}
<div className="flex items-center gap-2 mb-3 px-3">
<category.icon className="w-5 h-5 text-nofx-gold" />
@@ -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)}

View File

@@ -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 (
<section className="py-24 relative overflow-hidden" style={{ background: '#0B0E11' }}>
<section className="py-24 relative overflow-hidden" style={{ background: '#F1ECE2' }}>
{/* Background Decoration */}
<div
className="absolute top-0 right-0 w-96 h-96 rounded-full blur-3xl opacity-30"
style={{ background: 'radial-gradient(circle, rgba(240, 185, 11, 0.1) 0%, transparent 70%)' }}
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)' }}
/>
<div className="max-w-6xl mx-auto px-4">
@@ -45,21 +45,21 @@ export default function AboutSection({ language }: AboutSectionProps) {
<motion.div
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full mb-6"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: 'rgba(224, 72, 59, 0.1)',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
>
<Terminal className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#F0B90B' }}>
<Terminal className="w-4 h-4" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#E0483B' }}>
{t('aboutNofx', language)}
</span>
</motion.div>
<h2 className="text-4xl lg:text-5xl font-bold mb-6" style={{ color: '#EAECEF' }}>
<h2 className="text-4xl lg:text-5xl font-bold mb-6" style={{ color: '#1A1813' }}>
{t('whatIsNofx', language)}
</h2>
<p className="text-lg mb-8 leading-relaxed" style={{ color: '#848E9C' }}>
<p className="text-lg mb-8 leading-relaxed" style={{ color: '#8A8478' }}>
{t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)}
</p>
@@ -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)',
}}
>
<div
className="w-10 h-10 rounded-lg flex items-center justify-center"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
style={{ background: 'rgba(224, 72, 59, 0.1)' }}
>
<feature.icon className="w-5 h-5" style={{ color: '#F0B90B' }} />
<feature.icon className="w-5 h-5" style={{ color: '#E0483B' }} />
</div>
<div>
<div className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
<div className="text-sm font-semibold" style={{ color: '#1A1813' }}>
{feature.title}
</div>
<div className="text-xs" style={{ color: '#5E6673' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{feature.desc}
</div>
</div>
@@ -107,36 +107,36 @@ export default function AboutSection({ language }: AboutSectionProps) {
<div
className="rounded-2xl overflow-hidden"
style={{
background: '#0D1117',
border: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
boxShadow: '0 6px 24px rgba(26, 24, 19, 0.12)',
}}
>
{/* Terminal Header */}
<div
className="flex items-center gap-2 px-4 py-3"
style={{ background: 'rgba(255, 255, 255, 0.03)', borderBottom: '1px solid rgba(255, 255, 255, 0.06)' }}
style={{ background: '#E8E2D5', borderBottom: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full" style={{ background: '#FF5F56' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#FFBD2E' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#27C93F' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#D6433A' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#E0483B' }} />
<div className="w-3 h-3 rounded-full" style={{ background: '#2E8B57' }} />
</div>
<span className="text-xs ml-2" style={{ color: '#5E6673' }}>terminal</span>
<span className="text-xs ml-2" style={{ color: '#8A8478' }}>terminal</span>
</div>
{/* Terminal Content */}
<div className="p-6 font-mono text-sm space-y-2">
<div style={{ color: '#5E6673' }}>$ git clone https://github.com/NoFxAiOS/nofx.git</div>
<div style={{ color: '#5E6673' }}>$ cd nofx && chmod +x start.sh</div>
<div style={{ color: '#5E6673' }}>$ ./start.sh start --build</div>
<div className="pt-2" style={{ color: '#F0B90B' }}>
<div style={{ color: '#8A8478' }}>$ git clone https://github.com/NoFxAiOS/nofx.git</div>
<div style={{ color: '#8A8478' }}>$ cd nofx && chmod +x start.sh</div>
<div style={{ color: '#8A8478' }}>$ ./start.sh start --build</div>
<div className="pt-2" style={{ color: '#E0483B' }}>
{t('startupMessages1', language)}
</div>
<div style={{ color: '#0ECB81' }}>
<div style={{ color: '#2E8B57' }}>
{t('startupMessages2', language)}
</div>
<div style={{ color: '#0ECB81' }}>
<div style={{ color: '#2E8B57' }}>
{t('startupMessages3', language)}
</div>
<motion.div
@@ -144,8 +144,8 @@ export default function AboutSection({ language }: AboutSectionProps) {
animate={{ opacity: [1, 0.5, 1] }}
transition={{ duration: 1.5, repeat: Infinity }}
>
<span style={{ color: '#F0B90B' }}></span>
<span style={{ color: '#EAECEF' }}>_</span>
<span style={{ color: '#E0483B' }}></span>
<span style={{ color: '#1A1813' }}>_</span>
</motion.div>
</div>
</div>

View File

@@ -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

View File

@@ -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)' }}
/>
<div>
<div className="font-semibold text-sm" style={{ color: '#EAECEF' }}>
<div className="font-semibold text-sm" style={{ color: '#1A1813' }}>
{authorName}
</div>
<div className="text-xs" style={{ color: '#5E6673' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{handle}
</div>
</div>
@@ -52,7 +52,7 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw
{/* X Logo */}
<div
className="w-6 h-6 flex items-center justify-center opacity-50 group-hover:opacity-100 transition-opacity"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
<svg viewBox="0 0 24 24" className="w-4 h-4" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
@@ -63,27 +63,27 @@ function TweetCard({ quote, authorName, handle, avatarUrl, tweetUrl, delay }: Tw
{/* Content */}
<p
className="text-sm leading-relaxed mb-4 line-clamp-4"
style={{ color: '#B7BDC6' }}
style={{ color: '#1A1813' }}
>
{quote}
</p>
{/* Footer */}
<div className="flex items-center gap-6 pt-3" style={{ borderTop: '1px solid rgba(255, 255, 255, 0.05)' }}>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#5E6673' }}>
<div className="flex items-center gap-6 pt-3" style={{ borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
<MessageCircle className="w-3.5 h-3.5" />
<span>Reply</span>
</div>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#5E6673' }}>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
<Repeat2 className="w-3.5 h-3.5" />
<span>Repost</span>
</div>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#5E6673' }}>
<div className="flex items-center gap-1.5 text-xs" style={{ color: '#8A8478' }}>
<Heart className="w-3.5 h-3.5" />
<span>Like</span>
</div>
<div className="ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
<ExternalLink className="w-3.5 h-3.5" style={{ color: '#F0B90B' }} />
<ExternalLink className="w-3.5 h-3.5" style={{ color: '#E0483B' }} />
</div>
</div>
</motion.a>
@@ -98,11 +98,11 @@ export default function CommunitySection({ language }: CommunitySectionProps) {
const tweets: TweetProps[] = []
return (
<section className="py-24 relative" style={{ background: '#0B0E11' }}>
<section className="py-24 relative" style={{ background: '#F1ECE2' }}>
{/* Background Decoration */}
<div
className="absolute right-0 top-1/2 -translate-y-1/2 w-96 h-96 rounded-full blur-3xl opacity-20"
style={{ background: 'radial-gradient(circle, rgba(29, 161, 242, 0.1) 0%, transparent 70%)' }}
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)' }}
/>
<div className="max-w-6xl mx-auto px-4 relative z-10">
@@ -113,11 +113,11 @@ export default function CommunitySection({ language }: CommunitySectionProps) {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '社区声音' : 'Community Voices'}
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
{language === 'zh' ? 'Community Voices' : 'Community Voices'}
</h2>
<p className="text-lg" style={{ color: '#848E9C' }}>
{language === 'zh' ? '看看大家怎么说' : 'See what others are saying'}
<p className="text-lg" style={{ color: '#8A8478' }}>
{language === 'zh' ? 'See what others are saying' : 'See what others are saying'}
</p>
</motion.div>
@@ -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)',
}}
>
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
{language === 'zh' ? '关注我们的 X' : 'Follow us on X'}
{language === 'zh' ? 'Follow us on X' : 'Follow us on X'}
</a>
</motion.div>
</div>

View File

@@ -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'
? '支持 DeepSeekGPTClaudeQwen 等多种大模型,自定义 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'
? 'BinanceOKXBybitHyperliquidAster 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 (
<section className="py-24 relative" style={{ background: '#0B0E11' }}>
<section className="py-24 relative" style={{ background: '#F1ECE2' }}>
{/* Background */}
<div
className="absolute inset-0 opacity-[0.02]"
className="absolute inset-0 opacity-[0.04]"
style={{
backgroundImage: `linear-gradient(#F0B90B 1px, transparent 1px), linear-gradient(90deg, #F0B90B 1px, transparent 1px)`,
backgroundImage: `linear-gradient(#E0483B 1px, transparent 1px), linear-gradient(90deg, #E0483B 1px, transparent 1px)`,
backgroundSize: '40px 40px',
}}
/>
@@ -77,12 +77,12 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#EAECEF' }}>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
{t('whyChooseNofx', language)}
</h2>
<p className="text-lg max-w-2xl mx-auto" style={{ color: '#848E9C' }}>
<p className="text-lg max-w-2xl mx-auto" style={{ color: '#8A8478' }}>
{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'}
</p>
</motion.div>
@@ -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) {
<div
className="absolute top-4 right-4 px-2 py-1 rounded text-xs font-medium"
style={{
background: 'rgba(240, 185, 11, 0.15)',
color: '#F0B90B',
background: 'rgba(224, 72, 59, 0.15)',
color: '#E0483B',
}}
>
{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 }}
>
<feature.icon
className="w-6 h-6"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
/>
</motion.div>
{/* Text */}
<h3
className="text-xl font-bold mb-3"
style={{ color: '#EAECEF' }}
style={{ color: '#1A1813' }}
>
{feature.title}
</h3>
<p
className="text-sm leading-relaxed"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{feature.desc}
</p>
{/* Hover Glow */}
<div
className="absolute -bottom-10 -right-10 w-32 h-32 rounded-full blur-3xl opacity-0 group-hover:opacity-30 transition-opacity duration-500"
style={{ background: '#F0B90B' }}
className="absolute -bottom-10 -right-10 w-32 h-32 rounded-full blur-3xl opacity-0 group-hover:opacity-20 transition-opacity duration-500"
style={{ background: '#E0483B' }}
/>
</motion.div>
))}
@@ -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) => (
<div
key={stat.label}
className="text-center p-4 rounded-xl"
style={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.05)',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
<div
className="text-2xl font-bold mb-1"
style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
color: '#E0483B',
}}
>
{stat.value}
</div>
<div className="text-xs" style={{ color: '#5E6673' }}>
<div className="text-xs" style={{ color: '#8A8478' }}>
{stat.label}
</div>
</div>

View File

@@ -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 (
<footer style={{ background: '#0B0E11', borderTop: '1px solid rgba(255, 255, 255, 0.06)' }}>
<footer style={{ background: '#F1ECE2', borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}>
<div className="max-w-6xl mx-auto px-4 py-8 md:py-12">
{/* Top Section */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 md:gap-10 mb-8 md:mb-12">
@@ -51,11 +51,11 @@ export default function FooterSection({ language }: FooterSectionProps) {
<div className="md:col-span-1">
<div className="flex items-center gap-3 mb-4">
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
<span className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<span className="text-xl font-bold" style={{ color: '#1A1813' }}>
NOFX
</span>
</div>
<p className="text-sm mb-6" style={{ color: '#5E6673' }}>
<p className="text-sm mb-6" style={{ color: '#8A8478' }}>
{t('futureStandardAI', language)}
</p>
{/* Social Icons */}
@@ -68,8 +68,8 @@ export default function FooterSection({ language }: FooterSectionProps) {
rel="noopener noreferrer"
className="w-9 h-9 rounded-lg flex items-center justify-center transition-all hover:scale-110"
style={{
background: 'rgba(255, 255, 255, 0.05)',
color: '#848E9C',
background: '#E8E2D5',
color: '#8A8478',
}}
title={link.name}
>
@@ -81,7 +81,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Links */}
<div>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#EAECEF' }}>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#1A1813' }}>
{t('links', language)}
</h4>
<ul className="space-y-3">
@@ -91,8 +91,8 @@ export default function FooterSection({ language }: FooterSectionProps) {
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm transition-colors hover:text-[#F0B90B]"
style={{ color: '#5E6673' }}
className="text-sm transition-colors hover:text-[#E0483B]"
style={{ color: '#8A8478' }}
>
{link.name}
</a>
@@ -103,7 +103,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Resources */}
<div>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#EAECEF' }}>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#1A1813' }}>
{t('resources', language)}
</h4>
<ul className="space-y-3">
@@ -113,8 +113,8 @@ export default function FooterSection({ language }: FooterSectionProps) {
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-sm transition-colors hover:text-[#F0B90B] inline-flex items-center gap-1"
style={{ color: '#5E6673' }}
className="text-sm transition-colors hover:text-[#E0483B] inline-flex items-center gap-1"
style={{ color: '#8A8478' }}
>
{link.name}
<ExternalLink className="w-3 h-3 opacity-50" />
@@ -126,7 +126,7 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Supporters */}
<div>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#EAECEF' }}>
<h4 className="text-sm font-semibold mb-4" style={{ color: '#1A1813' }}>
{t('supporters', language)}
</h4>
<div className="flex flex-wrap gap-2">
@@ -136,8 +136,8 @@ export default function FooterSection({ language }: FooterSectionProps) {
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="text-xs border border-zinc-800 bg-zinc-900/50 rounded px-3 py-1.5 transition-all hover:border-[#F0B90B] hover:text-[#F0B90B] hover:bg-[#F0B90B]/10 hover:shadow-[0_0_10px_rgba(240,185,11,0.2)]"
style={{ color: '#848E9C' }}
className="text-xs border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter rounded px-3 py-1.5 transition-all hover:border-[#E0483B] hover:text-[#E0483B] hover:bg-[#E0483B]/10"
style={{ color: '#8A8478' }}
>
{link.name}
</a>
@@ -149,10 +149,10 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Bottom Section */}
<div
className="pt-6 text-center text-xs"
style={{ color: '#5E6673', borderTop: '1px solid rgba(255, 255, 255, 0.06)' }}
style={{ color: '#8A8478', borderTop: '1px solid rgba(26, 24, 19, 0.14)' }}
>
<p className="mb-2">{t('footerTitle', language)}</p>
<p style={{ color: '#3C4249' }}>{t('footerWarning', language)}</p>
<p style={{ color: '#B4B2A9' }}>{t('footerWarning', language)}</p>
</div>
</div>
</footer>

View File

@@ -26,7 +26,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `linear-gradient(#F0B90B 1px, transparent 1px), linear-gradient(90deg, #F0B90B 1px, transparent 1px)`,
backgroundImage: `linear-gradient(#E0483B 1px, transparent 1px), linear-gradient(90deg, #E0483B 1px, transparent 1px)`,
backgroundSize: '60px 60px',
}}
/>
@@ -35,13 +35,13 @@ export default function HeroSection({ language }: HeroSectionProps) {
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full"
style={{
background:
'radial-gradient(circle, rgba(240, 185, 11, 0.08) 0%, transparent 70%)',
'radial-gradient(circle, rgba(224, 72, 59, 0.08) 0%, transparent 70%)',
}}
/>
{/* Floating Orbs */}
<motion.div
className="absolute top-20 right-20 w-32 h-32 rounded-full blur-3xl"
style={{ background: 'rgba(240, 185, 11, 0.15)' }}
style={{ background: 'rgba(224, 72, 59, 0.12)' }}
animate={{
y: [0, 30, 0],
scale: [1, 1.1, 1],
@@ -50,7 +50,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
/>
<motion.div
className="absolute bottom-40 left-20 w-48 h-48 rounded-full blur-3xl"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
style={{ background: 'rgba(224, 72, 59, 0.08)' }}
animate={{
y: [0, -40, 0],
scale: [1, 1.2, 1],
@@ -67,21 +67,20 @@ export default function HeroSection({ language }: HeroSectionProps) {
transition={{ duration: 0.6 }}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-8"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
background: 'rgba(224, 72, 59, 0.1)',
border: '1px solid rgba(224, 72, 59, 0.3)',
}}
>
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-medium" style={{ color: '#F0B90B' }}>
<Zap className="w-4 h-4" style={{ color: '#E0483B' }} />
<span className="text-sm font-medium" style={{ color: '#E0483B' }}>
{isLoading ? (
t('githubStarsInDays', language)
) : language === 'zh' ? (
<>
{daysOld} {' '}
<span className="font-bold tabular-nums">
{(animatedStars / 1000).toFixed(1)}K+
</span>{' '}
GitHub Stars
GitHub Stars in {daysOld} days
</>
) : (
<>
@@ -101,20 +100,18 @@ export default function HeroSection({ language }: HeroSectionProps) {
transition={{ duration: 0.6, delay: 0.1 }}
className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 leading-tight"
>
<span style={{ color: '#EAECEF' }}>{t('heroTitle1', language)}</span>
<span style={{ color: '#1A1813' }}>{t('heroTitle1', language)}</span>
<br />
<span
className="relative inline-block"
style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
color: '#E0483B',
}}
>
{t('heroTitle2', language)}
<motion.span
className="absolute -bottom-2 left-0 h-1 rounded-full"
style={{ background: 'linear-gradient(90deg, #F0B90B, #FCD535)' }}
style={{ background: '#E0483B' }}
initial={{ width: 0 }}
animate={{ width: '100%' }}
transition={{ duration: 0.8, delay: 0.8 }}
@@ -128,7 +125,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-lg sm:text-xl max-w-3xl mx-auto mb-10 leading-relaxed"
style={{ color: '#848E9C' }}
style={{ color: '#8A8478' }}
>
{t('heroDescription', language)}
</motion.p>
@@ -143,13 +140,13 @@ export default function HeroSection({ language }: HeroSectionProps) {
<motion.div
className="group flex items-center gap-3 px-8 py-4 rounded-xl font-bold text-lg transition-all"
style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
color: '#0B0E11',
boxShadow: '0 4px 24px rgba(240, 185, 11, 0.3)',
background: '#E0483B',
color: '#F7F4EC',
boxShadow: '0 4px 24px rgba(224, 72, 59, 0.25)',
}}
whileHover={{
scale: 1.02,
boxShadow: '0 8px 32px rgba(240, 185, 11, 0.4)',
boxShadow: '0 8px 32px rgba(224, 72, 59, 0.35)',
}}
whileTap={{ scale: 0.98 }}
>
@@ -166,14 +163,14 @@ export default function HeroSection({ language }: HeroSectionProps) {
rel="noopener noreferrer"
className="group flex items-center gap-3 px-8 py-4 rounded-xl font-bold text-lg transition-all"
style={{
background: 'rgba(255, 255, 255, 0.05)',
color: '#EAECEF',
border: '1px solid rgba(255, 255, 255, 0.1)',
background: '#F7F4EC',
color: '#1A1813',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
whileHover={{
scale: 1.02,
background: 'rgba(255, 255, 255, 0.08)',
borderColor: 'rgba(240, 185, 11, 0.3)',
background: '#F2EEE4',
borderColor: 'rgba(224, 72, 59, 0.3)',
}}
whileTap={{ scale: 0.98 }}
>
@@ -192,15 +189,15 @@ export default function HeroSection({ language }: HeroSectionProps) {
{[
{ label: 'GitHub Stars', value: `${(stars / 1000).toFixed(1)}K+` },
{
label: language === 'zh' ? '支持交易所' : 'Exchanges',
label: language === 'zh' ? 'Exchanges' : 'Exchanges',
value: '5+',
},
{
label: language === 'zh' ? 'AI 模型' : 'AI Models',
label: language === 'zh' ? 'AI Models' : 'AI Models',
value: '10+',
},
{
label: language === 'zh' ? '开源免费' : 'Open Source',
label: language === 'zh' ? 'Open Source' : 'Open Source',
value: '100%',
},
].map((stat, index) => (
@@ -214,15 +211,12 @@ export default function HeroSection({ language }: HeroSectionProps) {
<div
className="text-3xl sm:text-4xl font-bold mb-1"
style={{
background:
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
color: '#E0483B',
}}
>
{stat.value}
</div>
<div className="text-sm" style={{ color: '#5E6673' }}>
<div className="text-sm" style={{ color: '#8A8478' }}>
{stat.label}
</div>
</motion.div>
@@ -235,7 +229,7 @@ export default function HeroSection({ language }: HeroSectionProps) {
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.8 }}
className="mt-16 text-xs"
style={{ color: '#5E6673' }}
style={{ color: '#8A8478' }}
>
{t('poweredBy', language)}
</motion.p>
@@ -250,13 +244,13 @@ export default function HeroSection({ language }: HeroSectionProps) {
>
<motion.div
className="w-6 h-10 rounded-full flex justify-center pt-2"
style={{ border: '2px solid rgba(240, 185, 11, 0.3)' }}
style={{ border: '2px solid rgba(224, 72, 59, 0.3)' }}
animate={{ y: [0, 8, 0] }}
transition={{ duration: 2, repeat: Infinity }}
>
<motion.div
className="w-1.5 h-3 rounded-full"
style={{ background: '#F0B90B' }}
style={{ background: '#E0483B' }}
/>
</motion.div>
</motion.div>

View File

@@ -11,38 +11,38 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
{
icon: Download,
number: '01',
title: language === 'zh' ? '一键部署' : 'One-Click Deploy',
title: language === 'zh' ? 'One-Click Deploy' : 'One-Click Deploy',
desc: language === 'zh'
? '在你的服务器上运行一条命令即可完成部署'
? 'Run a single command on your server to deploy'
: 'Run a single command on your server to deploy',
code: 'curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash',
},
{
icon: Rocket,
number: '02',
title: language === 'zh' ? '访问面板' : 'Access Dashboard',
title: language === 'zh' ? 'Access Dashboard' : 'Access Dashboard',
desc: language === 'zh'
? '通过浏览器访问你的服务器'
? 'Access your server via browser'
: 'Access your server via browser',
code: 'http://YOUR_SERVER_IP:3000',
},
{
icon: TrendingUp,
number: '03',
title: language === 'zh' ? '开始交易' : 'Start Trading',
title: language === 'zh' ? 'Start Trading' : 'Start Trading',
desc: language === 'zh'
? '创建交易员,让 AI 开始工作'
? 'Create trader, let AI do the work'
: 'Create trader, let AI do the work',
code: language === 'zh' ? '配置模型 → 配置交易所 → 创建交易员' : 'Configure Model → Exchange → Create Trader',
code: language === 'zh' ? 'Configure Model → Exchange → Create Trader' : 'Configure Model → Exchange → Create Trader',
},
]
return (
<section className="py-24 relative overflow-hidden" style={{ background: '#0D1117' }}>
<section className="py-24 relative overflow-hidden" style={{ background: '#F1ECE2' }}>
{/* Background Decoration */}
<div
className="absolute left-0 top-1/2 -translate-y-1/2 w-96 h-96 rounded-full blur-3xl opacity-20"
style={{ background: 'radial-gradient(circle, rgba(240, 185, 11, 0.15) 0%, transparent 70%)' }}
style={{ background: 'radial-gradient(circle, rgba(224, 72, 59, 0.12) 0%, transparent 70%)' }}
/>
<div className="max-w-6xl mx-auto px-4 relative z-10">
@@ -53,10 +53,10 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#EAECEF' }}>
<h2 className="text-4xl lg:text-5xl font-bold mb-4" style={{ color: '#1A1813' }}>
{t('howToStart', language)}
</h2>
<p className="text-lg" style={{ color: '#848E9C' }}>
<p className="text-lg" style={{ color: '#8A8478' }}>
{t('fourSimpleSteps', language)}
</p>
</motion.div>
@@ -66,7 +66,7 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
{/* Connecting Line */}
<div
className="absolute left-[39px] top-0 bottom-0 w-px hidden lg:block"
style={{ background: 'linear-gradient(to bottom, transparent, rgba(240, 185, 11, 0.3), transparent)' }}
style={{ background: 'linear-gradient(to bottom, transparent, rgba(224, 72, 59, 0.3), transparent)' }}
/>
<div className="space-y-6">
@@ -82,8 +82,8 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<div
className="flex flex-col lg:flex-row items-start gap-6 p-6 rounded-2xl transition-all duration-300 hover:translate-x-2"
style={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.05)',
background: '#F7F4EC',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
{/* Number Circle */}
@@ -91,12 +91,12 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<motion.div
className="w-20 h-20 rounded-2xl flex items-center justify-center"
style={{
background: 'linear-gradient(135deg, rgba(240, 185, 11, 0.2) 0%, rgba(240, 185, 11, 0.05) 100%)',
border: '1px solid rgba(240, 185, 11, 0.3)',
background: 'linear-gradient(135deg, rgba(224, 72, 59, 0.15) 0%, rgba(224, 72, 59, 0.05) 100%)',
border: '1px solid rgba(224, 72, 59, 0.3)',
}}
whileHover={{ scale: 1.1 }}
>
<step.icon className="w-8 h-8" style={{ color: '#F0B90B' }} />
<step.icon className="w-8 h-8" style={{ color: '#E0483B' }} />
</motion.div>
</div>
@@ -105,15 +105,15 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<div className="flex items-center gap-3 mb-2">
<span
className="text-sm font-mono font-bold"
style={{ color: '#F0B90B' }}
style={{ color: '#E0483B' }}
>
{step.number}
</span>
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
<h3 className="text-xl font-bold" style={{ color: '#1A1813' }}>
{step.title}
</h3>
</div>
<p className="mb-4" style={{ color: '#848E9C' }}>
<p className="mb-4" style={{ color: '#8A8478' }}>
{step.desc}
</p>
@@ -121,12 +121,12 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<div
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg font-mono text-sm"
style={{
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(255, 255, 255, 0.06)',
background: '#E8E2D5',
border: '1px solid rgba(26, 24, 19, 0.14)',
}}
>
<span style={{ color: '#5E6673' }}>$</span>
<span style={{ color: '#EAECEF' }}>{step.code}</span>
<span style={{ color: '#8A8478' }}>$</span>
<span style={{ color: '#1A1813' }}>{step.code}</span>
</div>
</div>
</div>
@@ -139,8 +139,8 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
<motion.div
className="mt-12 p-6 rounded-2xl flex items-start gap-4"
style={{
background: 'rgba(240, 185, 11, 0.05)',
border: '1px solid rgba(240, 185, 11, 0.15)',
background: 'rgba(224, 72, 59, 0.05)',
border: '1px solid rgba(224, 72, 59, 0.15)',
}}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -148,15 +148,15 @@ export default function HowItWorksSection({ language }: HowItWorksSectionProps)
>
<div
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
style={{ background: 'rgba(224, 72, 59, 0.1)' }}
>
<AlertTriangle className="w-6 h-6" style={{ color: '#F0B90B' }} />
<AlertTriangle className="w-6 h-6" style={{ color: '#E0483B' }} />
</div>
<div>
<div className="font-semibold mb-2" style={{ color: '#F0B90B' }}>
<div className="font-semibold mb-2" style={{ color: '#E0483B' }}>
{t('importantRiskWarning', language)}
</div>
<p className="text-sm leading-relaxed" style={{ color: '#5E6673' }}>
<p className="text-sm leading-relaxed" style={{ color: '#8A8478' }}>
{t('riskWarningText', language)}
</p>
</div>

View File

@@ -13,7 +13,7 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
return (
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ background: 'rgba(0, 0, 0, 0.8)' }}
style={{ background: 'rgba(26, 24, 19, 0.55)' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -22,8 +22,8 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
<motion.div
className="relative max-w-md w-full rounded-2xl p-8"
style={{
background: 'var(--brand-dark-gray)',
border: '1px solid rgba(240, 185, 11, 0.2)',
background: '#F7F4EC',
border: '1px solid rgba(224, 72, 59, 0.2)',
}}
initial={{ scale: 0.9, y: 50 }}
animate={{ scale: 1, y: 0 }}
@@ -33,7 +33,7 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
<motion.button
onClick={onClose}
className="absolute top-4 right-4"
style={{ color: 'var(--text-secondary)' }}
style={{ color: '#8A8478' }}
whileHover={{ scale: 1.1, rotate: 90 }}
whileTap={{ scale: 0.9 }}
>
@@ -41,11 +41,11 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
</motion.button>
<h2
className="text-2xl font-bold mb-6"
style={{ color: 'var(--brand-light-gray)' }}
style={{ color: '#1A1813' }}
>
{t('accessNofxPlatform', language)}
</h2>
<p className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
<p className="text-sm mb-6" style={{ color: '#8A8478' }}>
{t('loginRegisterPrompt', language)}
</p>
<div className="space-y-3">
@@ -56,12 +56,12 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
}}
className="block w-full px-6 py-3 rounded-lg font-semibold text-center"
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
background: '#E0483B',
color: '#F7F4EC',
}}
whileHover={{
scale: 1.05,
boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)',
boxShadow: '0 10px 30px rgba(224, 72, 59, 0.3)',
}}
whileTap={{ scale: 0.95 }}
>

View File

@@ -9,47 +9,42 @@ export default function AgentTerminal() {
className="w-[380px] lg:w-[440px] relative group"
>
{/* Terminal frame */}
<div className="relative bg-[#0B0F14] rounded-2xl overflow-hidden shadow-2xl shadow-black/80 border border-zinc-800/80">
{/* Scanline overlay */}
<div className="absolute inset-0 pointer-events-none z-50 opacity-[0.02]" style={{
backgroundImage: 'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.03) 2px, rgba(255,255,255,0.03) 4px)'
}} />
<div className="relative bg-nofx-bg-lighter rounded-2xl overflow-hidden shadow-lg border border-[rgba(26,24,19,0.14)]">
{/* Header bar - macOS style */}
<div className="flex items-center justify-between px-4 py-2.5 bg-[#0D1117] border-b border-zinc-800/60">
<div className="flex items-center justify-between px-4 py-2.5 bg-nofx-bg-deeper border-b border-[rgba(26,24,19,0.14)]">
{/* Window controls */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-[#ff5f57] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#febc2e] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#28c840] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#D6433A] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#E0483B] hover:brightness-110 transition-all" />
<div className="w-3 h-3 rounded-full bg-[#2E8B57] hover:brightness-110 transition-all" />
</div>
</div>
{/* Title */}
<div className="absolute left-1/2 -translate-x-1/2 flex items-center gap-2">
<span className="text-zinc-400 text-xs font-mono">NOFX Trader Terminal</span>
<span className="text-nofx-text-muted text-xs font-mono">NOFX Trader Terminal</span>
</div>
{/* Live indicator */}
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded bg-green-500/10 border border-green-500/20">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse" />
<span className="text-green-400 text-[10px] font-mono uppercase tracking-wider">Live</span>
<div className="flex items-center gap-1.5 px-2 py-0.5 rounded bg-nofx-success/10 border border-nofx-success/20">
<div className="w-1.5 h-1.5 bg-nofx-success rounded-full animate-pulse" />
<span className="text-nofx-success text-[10px] font-mono uppercase tracking-wider">Live</span>
</div>
</div>
{/* Portfolio PnL Section */}
<div className="p-4 border-b border-zinc-800/40">
<div className="p-4 border-b border-[rgba(26,24,19,0.14)]">
<div className="flex items-center justify-between mb-3">
<span className="text-zinc-500 text-xs font-mono uppercase tracking-wider">Portfolio PnL</span>
<span className="text-nofx-text-muted text-xs font-mono uppercase tracking-wider">Portfolio PnL</span>
<div className="flex gap-1">
<button className="px-2 py-0.5 bg-nofx-gold/20 border border-nofx-gold/30 rounded text-[10px] text-nofx-gold font-mono">24H</button>
<button className="px-2 py-0.5 text-[10px] text-zinc-600 font-mono hover:text-zinc-400 transition-colors">7D</button>
<button className="px-2 py-0.5 text-[10px] text-zinc-600 font-mono hover:text-zinc-400 transition-colors">30D</button>
<button className="px-2 py-0.5 text-[10px] text-nofx-text-muted font-mono hover:text-nofx-text transition-colors">7D</button>
<button className="px-2 py-0.5 text-[10px] text-nofx-text-muted font-mono hover:text-nofx-text transition-colors">30D</button>
</div>
</div>
<div className="flex items-baseline gap-3">
<span className="text-3xl font-bold text-green-400 font-mono tracking-tight">+$12,847.50</span>
<span className="text-green-500/80 text-sm font-mono">+8.42%</span>
<span className="text-3xl font-bold text-nofx-success font-mono tracking-tight">+$12,847.50</span>
<span className="text-nofx-success/80 text-sm font-mono">+8.42%</span>
</div>
{/* Chart Area */}
@@ -57,8 +52,8 @@ export default function AgentTerminal() {
<svg className="w-full h-full" preserveAspectRatio="none" viewBox="0 0 400 64">
<defs>
<linearGradient id="chartGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#22C55E" stopOpacity="0.2" />
<stop offset="100%" stopColor="#22C55E" stopOpacity="0" />
<stop offset="0%" stopColor="#2E8B57" stopOpacity="0.2" />
<stop offset="100%" stopColor="#2E8B57" stopOpacity="0" />
</linearGradient>
</defs>
<path
@@ -68,7 +63,7 @@ export default function AgentTerminal() {
<path
d="M0,56 C40,52 80,48 120,40 C160,32 200,28 240,24 C280,20 320,16 360,12 L400,8"
fill="none"
stroke="#22C55E"
stroke="#2E8B57"
strokeWidth="1.5"
/>
</svg>
@@ -76,32 +71,32 @@ export default function AgentTerminal() {
</div>
{/* Metrics Row */}
<div className="grid grid-cols-3 divide-x divide-zinc-800/40 border-b border-zinc-800/40">
<div className="grid grid-cols-3 divide-x divide-[rgba(26,24,19,0.14)] border-b border-[rgba(26,24,19,0.14)]">
<div className="p-3 text-center">
<div className="text-zinc-500 text-[10px] font-mono uppercase tracking-wider mb-1">OI</div>
<div className="text-white font-bold font-mono">$847M</div>
<div className="text-green-500 text-[10px] font-mono"> 2.1%</div>
<div className="text-nofx-text-muted text-[10px] font-mono uppercase tracking-wider mb-1">OI</div>
<div className="text-nofx-text font-bold font-mono">$847M</div>
<div className="text-nofx-success text-[10px] font-mono"> 2.1%</div>
</div>
<div className="p-3 text-center">
<div className="text-zinc-500 text-[10px] font-mono uppercase tracking-wider mb-1">Netflow</div>
<div className="text-green-400 font-bold font-mono">+$124M</div>
<div className="text-zinc-500 text-[10px] font-mono">24h inflow</div>
<div className="text-nofx-text-muted text-[10px] font-mono uppercase tracking-wider mb-1">Netflow</div>
<div className="text-nofx-success font-bold font-mono">+$124M</div>
<div className="text-nofx-text-muted text-[10px] font-mono">24h inflow</div>
</div>
<div className="p-3 text-center">
<div className="text-zinc-500 text-[10px] font-mono uppercase tracking-wider mb-1">L/S Ratio</div>
<div className="text-white font-bold font-mono">1.24</div>
<div className="text-nofx-text-muted text-[10px] font-mono uppercase tracking-wider mb-1">L/S Ratio</div>
<div className="text-nofx-text font-bold font-mono">1.24</div>
<div className="flex gap-0.5 mt-1 px-2">
<div className="h-1 bg-green-500/60 rounded-l flex-[55]" />
<div className="h-1 bg-red-500/60 rounded-r flex-[45]" />
<div className="h-1 bg-nofx-success/60 rounded-l flex-[55]" />
<div className="h-1 bg-nofx-danger/60 rounded-r flex-[45]" />
</div>
</div>
</div>
{/* Order Book */}
<div className="p-4 border-b border-zinc-800/40">
<div className="p-4 border-b border-[rgba(26,24,19,0.14)]">
<div className="flex items-center justify-between mb-3">
<span className="text-zinc-400 text-xs font-mono uppercase tracking-wider">Order Book</span>
<span className="text-zinc-600 text-[10px] font-mono">Spread: <span className="text-nofx-gold">0.02%</span></span>
<span className="text-nofx-text text-xs font-mono uppercase tracking-wider">Order Book</span>
<span className="text-nofx-text-muted text-[10px] font-mono">Spread: <span className="text-nofx-gold">0.02%</span></span>
</div>
<div className="grid grid-cols-2 gap-3">
{/* Asks */}
@@ -112,9 +107,9 @@ export default function AgentTerminal() {
{ price: '97,251.00', amount: '0.945', depth: 30 },
].map((ask, i) => (
<div key={i} className="relative flex justify-between text-[11px] py-1 px-1.5 rounded">
<div className="absolute inset-0 bg-red-500/10 rounded-sm" style={{ width: `${ask.depth}%` }} />
<span className="relative text-red-400 font-mono">{ask.price}</span>
<span className="relative text-zinc-500 font-mono">{ask.amount}</span>
<div className="absolute inset-0 bg-nofx-danger/10 rounded-sm" style={{ width: `${ask.depth}%` }} />
<span className="relative text-nofx-danger font-mono">{ask.price}</span>
<span className="relative text-nofx-text-muted font-mono">{ask.amount}</span>
</div>
))}
</div>
@@ -126,9 +121,9 @@ export default function AgentTerminal() {
{ price: '97,198.00', amount: '1.845', depth: 50 },
].map((bid, i) => (
<div key={i} className="relative flex justify-between text-[11px] py-1 px-1.5 rounded">
<div className="absolute inset-0 bg-green-500/10 rounded-sm" style={{ width: `${bid.depth}%` }} />
<span className="relative text-green-400 font-mono">{bid.price}</span>
<span className="relative text-zinc-500 font-mono">{bid.amount}</span>
<div className="absolute inset-0 bg-nofx-success/10 rounded-sm" style={{ width: `${bid.depth}%` }} />
<span className="relative text-nofx-success font-mono">{bid.price}</span>
<span className="relative text-nofx-text-muted font-mono">{bid.amount}</span>
</div>
))}
</div>
@@ -138,8 +133,8 @@ export default function AgentTerminal() {
{/* Active Positions */}
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-zinc-400 text-xs font-mono uppercase tracking-wider">Positions</span>
<span className="text-green-400 text-xs font-mono font-medium">+$12,847</span>
<span className="text-nofx-text text-xs font-mono uppercase tracking-wider">Positions</span>
<span className="text-nofx-success text-xs font-mono font-medium">+$12,847</span>
</div>
<div className="space-y-2">
{[
@@ -147,7 +142,7 @@ export default function AgentTerminal() {
{ coin: 'ETH', name: 'ETH-PERP', size: '3.2', profit: '+$4,127', percent: '+7.6%', color: '#627EEA' },
{ coin: 'BNB', name: 'BNB-PERP', size: '8.5', profit: '+$2,300', percent: '+5.2%', color: '#F3BA2F' },
].map((pos, i) => (
<div key={i} className="flex items-center justify-between py-2 px-2 rounded-lg bg-zinc-900/50 hover:bg-zinc-800/50 transition-colors">
<div key={i} className="flex items-center justify-between py-2 px-2 rounded-lg bg-nofx-bg-deeper hover:bg-nofx-bg transition-colors">
<div className="flex items-center gap-3">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold border"
@@ -160,16 +155,16 @@ export default function AgentTerminal() {
{pos.coin}
</div>
<div>
<div className="text-white text-sm font-mono">{pos.name}</div>
<div className="text-nofx-text text-sm font-mono">{pos.name}</div>
<div className="flex items-center gap-2 text-[10px]">
<span className="text-green-400 bg-green-500/10 px-1.5 py-0.5 rounded font-mono">LONG</span>
<span className="text-zinc-500 font-mono">{pos.size} {pos.coin}</span>
<span className="text-nofx-success bg-nofx-success/10 px-1.5 py-0.5 rounded font-mono">LONG</span>
<span className="text-nofx-text-muted font-mono">{pos.size} {pos.coin}</span>
</div>
</div>
</div>
<div className="text-right">
<div className="text-green-400 font-mono font-medium">{pos.profit}</div>
<div className="text-green-500/70 text-[10px] font-mono">{pos.percent}</div>
<div className="text-nofx-success font-mono font-medium">{pos.profit}</div>
<div className="text-nofx-success/70 text-[10px] font-mono">{pos.percent}</div>
</div>
</div>
))}
@@ -177,15 +172,15 @@ export default function AgentTerminal() {
</div>
{/* Footer status bar */}
<div className="px-4 py-2 bg-[#0D1117] border-t border-zinc-800/60 flex items-center justify-between">
<div className="flex items-center gap-3 text-[10px] font-mono text-zinc-600">
<div className="px-4 py-2 bg-nofx-bg-deeper border-t border-[rgba(26,24,19,0.14)] flex items-center justify-between">
<div className="flex items-center gap-3 text-[10px] font-mono text-nofx-text-muted">
<span className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
<div className="w-1.5 h-1.5 bg-nofx-success rounded-full" />
Connected
</span>
<span>Latency: 12ms</span>
</div>
<div className="text-[10px] font-mono text-zinc-600">
<div className="text-[10px] font-mono text-nofx-text-muted">
mainnet v2.4.0
</div>
</div>

View File

@@ -36,14 +36,14 @@ const features = [
export default function BrandFeatures() {
return (
<section id="features" className="py-24 bg-zinc-950 relative">
<section id="features" className="py-24 bg-nofx-bg relative">
<div className="max-w-[1920px] mx-auto px-6 lg:px-16">
<div className="mb-16 border-l-4 border-nofx-gold pl-6">
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter mb-4">
Core Protocol <span className="text-zinc-600">Specs</span>
<h2 className="text-4xl md:text-5xl font-black text-nofx-text uppercase tracking-tighter mb-4">
Core Protocol <span className="text-nofx-text-muted">Specs</span>
</h2>
<p className="text-xl text-zinc-400 font-mono">
<p className="text-xl text-nofx-text-muted font-mono">
Next generation infrastructure for algorithmic dominance.
</p>
</div>
@@ -52,7 +52,7 @@ export default function BrandFeatures() {
{features.map((f, i) => (
<motion.div
key={i}
className="group relative bg-zinc-900 border border-zinc-800 p-8 hover:bg-zinc-800 transition-colors cursor-default overflow-hidden"
className="group relative bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] p-8 hover:bg-nofx-bg-deeper transition-colors cursor-default overflow-hidden"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
@@ -64,11 +64,11 @@ export default function BrandFeatures() {
<f.icon className="w-10 h-10 text-nofx-gold mb-6" />
<h3 className="text-xl font-bold text-white mb-3 uppercase flex items-center gap-2">
<h3 className="text-xl font-bold text-nofx-text mb-3 uppercase flex items-center gap-2">
{f.title}
</h3>
<p className="text-zinc-400 leading-relaxed text-sm md:text-base">
<p className="text-nofx-text-muted leading-relaxed text-sm md:text-base">
{f.description}
</p>

View File

@@ -16,7 +16,7 @@ export default function BrandHero() {
<section className="relative w-full min-h-screen bg-nofx-bg text-nofx-text overflow-hidden flex flex-col pt-16">
{/* Top Marquee */}
<div className="w-full bg-nofx-gold text-black font-bold py-2 border-y border-black z-20">
<div className="w-full bg-nofx-gold text-nofx-bg-lighter font-bold py-2 border-y border-nofx-text/20 z-20">
<Marquee speed={40}>
<span className="mx-8 text-sm md:text-base uppercase tracking-widest">NOFX AI TRADING AUTOMATED WEALTH DECENTRALIZED INTELLIGENCE PUNK ETHOS </span>
<span className="mx-8 text-sm md:text-base uppercase tracking-widest">NOFX AI TRADING AUTOMATED WEALTH DECENTRALIZED INTELLIGENCE PUNK ETHOS </span>
@@ -37,17 +37,17 @@ export default function BrandHero() {
<span className="text-nofx-gold">EVOLVED</span>
</h1>
<p className="text-xl md:text-2xl text-zinc-400 max-w-xl mb-10 font-mono leading-relaxed">
<p className="text-xl md:text-2xl text-nofx-text-muted max-w-xl mb-10 font-mono leading-relaxed">
Autonomous trading agents. High-frequency execution.
<br />
Institutional-grade strategies for the
<span className="text-white font-bold ml-2 bg-nofx-accent px-2 py-0.5">DEGENERATES</span>.
<span className="text-nofx-bg-lighter font-bold ml-2 bg-nofx-accent px-2 py-0.5">DEGENERATES</span>.
</p>
<div className="flex flex-wrap gap-4">
<button
onClick={handleScroll}
className="bg-nofx-gold text-black text-lg font-black px-8 py-4 uppercase tracking-wider hover:bg-white hover:scale-105 transition-all flex items-center gap-2 clip-path-slant"
className="bg-nofx-gold text-nofx-bg-lighter text-lg font-black px-8 py-4 uppercase tracking-wider hover:bg-nofx-text hover:scale-105 transition-all flex items-center gap-2 clip-path-slant"
style={{ clipPath: 'polygon(0 0, 100% 0, 95% 100%, 0% 100%)' }}
>
Start Trading <ArrowRight className="w-6 h-6" />
@@ -57,15 +57,15 @@ export default function BrandHero() {
href={OFFICIAL_LINKS.github}
target="_blank"
rel="noreferrer"
className="border-2 border-white/20 text-white text-lg font-bold px-8 py-4 uppercase tracking-wider hover:bg-white/10 hover:border-white transition-all flex items-center gap-2"
className="border-2 border-[rgba(26,24,19,0.2)] text-nofx-text text-lg font-bold px-8 py-4 uppercase tracking-wider hover:bg-nofx-text/5 hover:border-nofx-text transition-all flex items-center gap-2"
>
<Github className="w-5 h-5" /> Source
</a>
</div>
<div className="mt-12 flex items-center gap-8 text-zinc-500 font-mono text-xs md:text-sm">
<div className="mt-12 flex items-center gap-8 text-nofx-text-muted font-mono text-xs md:text-sm">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<div className="w-2 h-2 bg-nofx-success rounded-full animate-pulse" />
SYSTEM ONLINE
</div>
<div className="flex items-center gap-2">
@@ -79,14 +79,14 @@ export default function BrandHero() {
{/* Right Visual - Trader Terminal */}
<div className="flex-1 relative overflow-visible flex items-center justify-center py-8 lg:py-0 min-h-[600px]">
{/* Background gradient orbs */}
<div className="absolute top-1/2 right-[15%] -translate-y-1/2 w-[450px] h-[450px] rounded-full bg-gradient-to-br from-nofx-gold/20 via-nofx-gold/5 to-transparent blur-[80px]" />
<div className="absolute top-[25%] right-[35%] w-[250px] h-[250px] rounded-full bg-nofx-accent/10 blur-[60px]" />
<div className="absolute top-1/2 right-[15%] -translate-y-1/2 w-[450px] h-[450px] rounded-full bg-nofx-gold/10 blur-[80px]" />
<div className="absolute top-[25%] right-[35%] w-[250px] h-[250px] rounded-full bg-nofx-accent/8 blur-[60px]" />
{/* Subtle dot grid */}
<div
className="absolute inset-0 opacity-[0.04]"
className="absolute inset-0 opacity-[0.05]"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.4) 1px, transparent 0)',
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(26,24,19,0.4) 1px, transparent 0)',
backgroundSize: '32px 32px'
}}
/>

View File

@@ -27,19 +27,19 @@ export default function BrandStats() {
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: i * 0.1 }}
className="relative overflow-hidden group bg-black/40 backdrop-blur-md border border-white/10 p-6 rounded-lg md:bg-transparent md:border-0 md:p-0 md:backdrop-blur-none"
className="relative overflow-hidden group bg-white/10 backdrop-blur-md border border-white/20 p-6 rounded-lg md:bg-transparent md:border-0 md:p-0 md:backdrop-blur-none"
>
{/* Mobile Neon Corners */}
<div className="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 border-nofx-gold md:hidden opacity-80 shadow-[0_0_10px_rgba(234,179,8,0.5)]"></div>
<div className="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 border-nofx-gold md:hidden opacity-80 shadow-[0_0_10px_rgba(234,179,8,0.5)]"></div>
{/* Mobile Corners */}
<div className="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 border-white md:hidden opacity-80"></div>
<div className="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 border-white md:hidden opacity-80"></div>
{/* Mobile Inner Glow */}
<div className="absolute inset-0 bg-nofx-gold/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none md:hidden"></div>
<div className="absolute inset-0 bg-white/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none md:hidden"></div>
<div className="text-3xl md:text-6xl font-black text-white tracking-tighter mb-2 group-hover:scale-110 transition-transform duration-300 origin-left relative z-10">
{stat.value}
</div>
<div className="text-[10px] md:text-base font-bold text-zinc-400 md:text-black/60 uppercase tracking-widest bg-white/5 md:bg-white/20 inline-block px-2 py-1 rounded relative z-10">
<div className="text-[10px] md:text-base font-bold text-white/80 md:text-white/80 uppercase tracking-widest bg-white/10 md:bg-white/20 inline-block px-2 py-1 rounded relative z-10">
{stat.label}
</div>
</motion.div>

View File

@@ -15,7 +15,7 @@ const traderPresets = [
risk: 'HIGH',
color: 'text-nofx-gold',
border: 'border-nofx-gold/50',
bg_glow: 'shadow-[0_0_30px_rgba(240,185,11,0.1)]',
bg_glow: 'shadow-sm',
icon: Zap,
},
{
@@ -25,9 +25,9 @@ const traderPresets = [
apy: '89%',
winRate: '55%',
risk: 'MED',
color: 'text-blue-400',
border: 'border-blue-400/30',
bg_glow: 'shadow-[0_0_30px_rgba(96,165,250,0.1)]',
color: 'text-nofx-accent',
border: 'border-nofx-accent/30',
bg_glow: 'shadow-sm',
icon: TrendingUp,
},
{
@@ -37,9 +37,9 @@ const traderPresets = [
apy: '24%',
winRate: '99%',
risk: 'LOW',
color: 'text-purple-400',
border: 'border-purple-400/30',
bg_glow: 'shadow-[0_0_30px_rgba(192,132,252,0.1)]',
color: 'text-nofx-text',
border: 'border-nofx-gold/20',
bg_glow: 'shadow-sm',
icon: Layers,
},
]
@@ -62,8 +62,8 @@ export default function AgentGrid() {
className="py-16 md:py-24 bg-nofx-bg relative overflow-hidden"
>
{/* Background Details */}
<div className="absolute top-0 right-0 p-10 opacity-20 pointer-events-none">
<Hexagon className="w-64 h-64 text-zinc-800" strokeWidth={0.5} />
<div className="absolute top-0 right-0 p-10 opacity-10 pointer-events-none">
<Hexagon className="w-64 h-64 text-nofx-text-muted" strokeWidth={0.5} />
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
@@ -72,14 +72,14 @@ export default function AgentGrid() {
<div className="flex items-center gap-2 text-nofx-gold font-mono text-xs mb-2 tracking-widest uppercase">
<Crosshair className="w-4 h-4" /> ASSET CLASS SELECT
</div>
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter">
<h2 className="text-4xl md:text-5xl font-black text-nofx-text uppercase tracking-tighter">
PROFESSIONAL{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">
<span className="text-nofx-gold">
TRADERS
</span>
</h2>
</div>
<div className="font-mono text-right text-xs text-zinc-500 max-w-xs">
<div className="font-mono text-right text-xs text-nofx-text-muted max-w-xs">
CREATE TRADERS FOR US STOCKS, COMMODITIES, FX AND PRE-IPO MARKETS.
DESCRIBE THE STRATEGY IN ONE SENTENCE.
</div>
@@ -96,19 +96,19 @@ export default function AgentGrid() {
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
className={`group relative bg-black/40 backdrop-blur-xl border ${preset.border} overflow-hidden transition-all duration-300 min-w-[85vw] md:min-w-0 snap-center shrink-0 rounded-xl md:rounded-none`}
className={`group relative bg-nofx-bg-lighter backdrop-blur-xl border ${preset.border} overflow-hidden transition-all duration-300 min-w-[85vw] md:min-w-0 snap-center shrink-0 rounded-xl md:rounded-none`}
>
{/* Top "Hinge" decoration */}
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-white/10 to-transparent"></div>
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-nofx-text/10 to-transparent"></div>
<div className="p-8 relative z-10">
{/* Header */}
<div className="flex justify-between items-start mb-6">
<div className="p-3 bg-zinc-900/80 rounded border border-zinc-700">
<div className="p-3 bg-nofx-bg-deeper rounded border border-[rgba(26,24,19,0.14)]">
<Icon className={`w-8 h-8 ${preset.color}`} />
</div>
<div className="text-right">
<div className="text-[10px] font-mono text-zinc-500 uppercase">
<div className="text-[10px] font-mono text-nofx-text-muted uppercase">
Class
</div>
<div
@@ -120,33 +120,33 @@ export default function AgentGrid() {
</div>
{/* Name & Desc */}
<h3 className="text-3xl font-bold text-white mb-2 tracking-tight group-hover:text-nofx-accent transition-colors">
<h3 className="text-3xl font-bold text-nofx-text mb-2 tracking-tight group-hover:text-nofx-accent transition-colors">
{preset.name}
</h3>
<p className="text-zinc-500 text-sm mb-8 leading-relaxed h-10">
<p className="text-nofx-text-muted text-sm mb-8 leading-relaxed h-10">
{preset.desc}
</p>
{/* Stats Grid */}
<div className="grid grid-cols-3 gap-px bg-zinc-800/50 border border-zinc-800 rounded overflow-hidden mb-8">
<div className="bg-black/60 p-3 text-center group-hover:bg-zinc-900/60 transition-colors">
<div className="text-[10px] text-zinc-500 uppercase font-mono mb-1">
<div className="grid grid-cols-3 gap-px bg-[rgba(26,24,19,0.14)] border border-[rgba(26,24,19,0.14)] rounded overflow-hidden mb-8">
<div className="bg-nofx-bg-deeper p-3 text-center group-hover:bg-nofx-bg transition-colors">
<div className="text-[10px] text-nofx-text-muted uppercase font-mono mb-1">
APY
</div>
<div className="text-green-400 font-bold">
<div className="text-nofx-success font-bold">
{preset.apy}
</div>
</div>
<div className="bg-black/60 p-3 text-center group-hover:bg-zinc-900/60 transition-colors">
<div className="text-[10px] text-zinc-500 uppercase font-mono mb-1">
<div className="bg-nofx-bg-deeper p-3 text-center group-hover:bg-nofx-bg transition-colors">
<div className="text-[10px] text-nofx-text-muted uppercase font-mono mb-1">
Win %
</div>
<div className="text-white font-bold">
<div className="text-nofx-text font-bold">
{preset.winRate}
</div>
</div>
<div className="bg-black/60 p-3 text-center group-hover:bg-zinc-900/60 transition-colors">
<div className="text-[10px] text-zinc-500 uppercase font-mono mb-1">
<div className="bg-nofx-bg-deeper p-3 text-center group-hover:bg-nofx-bg transition-colors">
<div className="text-[10px] text-nofx-text-muted uppercase font-mono mb-1">
Risk
</div>
<div className={`${preset.color} font-bold`}>
@@ -158,7 +158,7 @@ export default function AgentGrid() {
{/* Action Btn */}
<button
onClick={handleInitialize}
className={`w-full py-4 text-xs font-bold font-mono uppercase tracking-[0.2em] border border-zinc-700 hover:border-${preset.color === 'text-nofx-gold' ? 'nofx-gold' : 'white'} hover:bg-white/5 transition-all flex items-center justify-center gap-2 group-hover:text-white cursor-pointer`}
className={`w-full py-4 text-xs font-bold font-mono uppercase tracking-[0.2em] border border-[rgba(26,24,19,0.14)] hover:border-${preset.color === 'text-nofx-gold' ? 'nofx-gold' : 'nofx-text'} hover:bg-nofx-text/5 transition-all flex items-center justify-center gap-2 group-hover:text-nofx-text cursor-pointer text-nofx-text`}
>
<span className={preset.color}>[</span> INITIALIZE{' '}
<span className={preset.color}>]</span>
@@ -166,8 +166,7 @@ export default function AgentGrid() {
</div>
{/* Decorative Background Elements */}
<div className="absolute -right-10 -bottom-10 w-40 h-40 bg-gradient-to-br from-white/5 to-transparent rounded-full blur-2xl group-hover:opacity-50 transition-opacity opacity-20"></div>
<div className="absolute inset-0 bg-scanlines opacity-20 pointer-events-none"></div>
<div className="absolute -right-10 -bottom-10 w-40 h-40 bg-gradient-to-br from-nofx-text/5 to-transparent rounded-full blur-2xl group-hover:opacity-50 transition-opacity opacity-20"></div>
</motion.div>
)
})}

View File

@@ -13,9 +13,9 @@ export default function DeploymentHub() {
}
return (
<section className="py-24 bg-black relative overflow-hidden border-t border-zinc-800">
<section className="py-24 bg-nofx-bg relative overflow-hidden border-t border-[rgba(26,24,19,0.14)]">
{/* Background Grids */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
<div className="absolute inset-0 bg-[linear-gradient(to_right,#1a181310_1px,transparent_1px),linear-gradient(to_bottom,#1a181310_1px,transparent_1px)] bg-[size:24px_24px]"></div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
@@ -26,11 +26,11 @@ export default function DeploymentHub() {
<Server className="w-4 h-4" /> System Deployment
</div>
<h2 className="text-4xl md:text-6xl font-black text-white leading-tight">
DEPLOY <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">INSTANTLY</span>
<h2 className="text-4xl md:text-6xl font-black text-nofx-text leading-tight">
DEPLOY <span className="text-nofx-gold">INSTANTLY</span>
</h2>
<p className="text-zinc-400 text-lg leading-relaxed font-light">
<p className="text-nofx-text-muted text-lg leading-relaxed font-light">
Initialize your own high-frequency trading node in seconds.
Our optimized installer handles all dependencies, bringing the trading system online with a single command.
</p>
@@ -40,13 +40,13 @@ export default function DeploymentHub() {
{ icon: Command, label: "One-Line Install", desc: "No configuration needed" },
{ icon: Shield, label: "Secure Core", desc: "Sandboxed execution env" }
].map((item, i) => (
<div key={i} className="flex gap-4 items-start p-4 rounded bg-zinc-900/50 border border-zinc-800 hover:border-nofx-gold/30 transition-colors group">
<div className="p-2 rounded bg-black border border-zinc-800 text-nofx-gold group-hover:bg-nofx-gold/10 transition-colors">
<div key={i} className="flex gap-4 items-start p-4 rounded bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] hover:border-nofx-gold/30 transition-colors group">
<div className="p-2 rounded bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] text-nofx-gold group-hover:bg-nofx-gold/10 transition-colors">
<item.icon className="w-5 h-5" />
</div>
<div>
<h4 className="text-white font-bold font-mono text-sm mb-1">{item.label}</h4>
<p className="text-zinc-500 text-xs">{item.desc}</p>
<h4 className="text-nofx-text font-bold font-mono text-sm mb-1">{item.label}</h4>
<p className="text-nofx-text-muted text-xs">{item.desc}</p>
</div>
</div>
))}
@@ -61,31 +61,31 @@ export default function DeploymentHub() {
className="relative"
>
{/* Glow effect */}
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold/20 to-blue-500/20 rounded-xl blur-xl opacity-50"></div>
<div className="absolute -inset-1 bg-nofx-gold/10 rounded-xl blur-xl opacity-50"></div>
<div className="relative rounded-xl overflow-hidden bg-[#0a0a0a] border border-zinc-800 shadow-2xl">
<div className="relative rounded-xl overflow-hidden bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] shadow-lg">
{/* Terminal Header */}
<div className="flex items-center justify-between px-4 py-3 bg-zinc-900/80 border-b border-zinc-800">
<div className="flex items-center justify-between px-4 py-3 bg-nofx-bg-deeper border-b border-[rgba(26,24,19,0.14)]">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/80"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500/80"></div>
<div className="w-3 h-3 rounded-full bg-green-500/80"></div>
<div className="w-3 h-3 rounded-full bg-nofx-danger/80"></div>
<div className="w-3 h-3 rounded-full bg-nofx-gold/80"></div>
<div className="w-3 h-3 rounded-full bg-nofx-success/80"></div>
</div>
<div className="text-[10px] font-mono text-zinc-500 flex items-center gap-1.5">
<div className="text-[10px] font-mono text-nofx-text-muted flex items-center gap-1.5">
<Terminal className="w-3 h-3" />
root@nofx-os:~
</div>
</div>
{/* Terminal Content */}
<div className="p-8 font-mono text-sm md:text-base bg-black/50 backdrop-blur-sm min-h-[200px] flex flex-col justify-center">
<div className="mb-2 text-zinc-500 text-xs tracking-wide"># Initialize NoFX Core Protocol</div>
<div className="p-8 font-mono text-sm md:text-base bg-nofx-bg-lighter min-h-[200px] flex flex-col justify-center">
<div className="mb-2 text-nofx-text-muted text-xs tracking-wide"># Initialize NoFX Core Protocol</div>
<div
className="group relative flex items-start gap-3 p-4 rounded-lg bg-zinc-900/50 border border-zinc-800 hover:border-nofx-gold/50 cursor-pointer transition-all hover:bg-zinc-900/80"
className="group relative flex items-start gap-3 p-4 rounded-lg bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] hover:border-nofx-gold/50 cursor-pointer transition-all hover:bg-nofx-bg"
onClick={handleCopy}
>
<span className="text-nofx-gold mt-1"><ChevronRight className="w-4 h-4" /></span>
<code className="text-zinc-100 flex-1 break-all">
<code className="text-nofx-text flex-1 break-all">
{installCmd}
</code>
@@ -96,12 +96,12 @@ export default function DeploymentHub() {
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.5, opacity: 0 }}
className="flex items-center gap-1 text-green-400 bg-green-400/10 px-2 py-1 rounded text-xs font-bold"
className="flex items-center gap-1 text-nofx-success bg-nofx-success/10 px-2 py-1 rounded text-xs font-bold"
>
<Check className="w-3 h-3" />
</motion.div>
) : (
<div className="text-zinc-400 bg-zinc-800 p-1.5 rounded hover:text-white hover:bg-zinc-700">
<div className="text-nofx-text-muted bg-nofx-bg-deeper p-1.5 rounded hover:text-nofx-text hover:bg-nofx-bg">
<Copy className="w-4 h-4" />
</div>
)}

View File

@@ -21,7 +21,7 @@ const generateLog = (id: number): LogEntry => {
switch (type) {
case 'EXEC':
msg = `AGENT-${Math.floor(Math.random() * 99)} ${actions[Math.floor(Math.random() * 4)]} ${pairs[Math.floor(Math.random() * pairs.length)]} @ ${Math.floor(Math.random() * 600)}`
color = 'text-green-500'
color = 'text-nofx-success'
break;
case 'SIGNAL':
msg = `US equities momentum signal confirmed (${(Math.random()).toFixed(3)} z-score)`
@@ -29,15 +29,15 @@ const generateLog = (id: number): LogEntry => {
break;
case 'RISK':
msg = `Risk check passed: ${pairs[Math.floor(Math.random() * pairs.length)]} exposure within limits`
color = 'text-red-500'
color = 'text-nofx-danger'
break;
case 'MACRO':
msg = `Macro feed latency < ${Math.floor(Math.random() * 10)}ms`
color = 'text-zinc-500'
color = 'text-nofx-text-muted'
break;
default:
msg = `System optimization cycle complete. Allocating resources.`
color = 'text-blue-400'
color = 'text-nofx-accent'
}
return { id, time: new Date().toLocaleTimeString('en-US', { hour12: false }) + '.' + Math.floor(Math.random() * 999), type, msg, color }
@@ -62,16 +62,15 @@ export default function LiveFeed() {
}, [])
return (
<section className="w-full bg-[#020304] border-y border-zinc-800 py-1 overflow-hidden relative">
<div className="absolute inset-0 bg-scanlines opacity-10 pointer-events-none"></div>
<section className="w-full bg-nofx-bg-lighter border-y border-[rgba(26,24,19,0.14)] py-1 overflow-hidden relative">
<div className="max-w-[1920px] mx-auto px-4 flex flex-col md:flex-row gap-0 md:gap-8 items-stretch h-[240px] md:h-12 text-xs font-mono">
{/* Left Status Bar (Static) */}
<div className="hidden md:flex items-center gap-6 text-zinc-600 border-r border-zinc-900 pr-6 shrink-0">
<div className="hidden md:flex items-center gap-6 text-nofx-text-muted border-r border-[rgba(26,24,19,0.14)] pr-6 shrink-0">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span className="font-bold text-zinc-400">WS_CONN: STABLE</span>
<div className="w-1.5 h-1.5 bg-nofx-success rounded-full animate-pulse"></div>
<span className="font-bold text-nofx-text">WS_CONN: STABLE</span>
</div>
<div className="flex items-center gap-2">
<span className="text-nofx-gold">TPS: 48,291</span>
@@ -90,10 +89,10 @@ export default function LiveFeed() {
animate={{ opacity: 1, x: 0 }}
className="absolute inset-0 flex items-center gap-4"
>
<span className="text-zinc-600">[{log.time}]</span>
<span className={`font-bold w-10 ${log.type === 'RISK' ? 'text-blue-400 bg-blue-500/10 px-1 rounded' :
<span className="text-nofx-text-muted">[{log.time}]</span>
<span className={`font-bold w-10 ${log.type === 'RISK' ? 'text-nofx-danger bg-nofx-danger/10 px-1 rounded' :
log.type === 'SIGNAL' ? 'text-nofx-gold bg-nofx-gold/10 px-1 rounded' :
log.type === 'EXEC' ? 'text-green-500' : 'text-zinc-500'
log.type === 'EXEC' ? 'text-nofx-success' : 'text-nofx-text-muted'
}`}>{log.type}</span>
<span className={`${log.color}`}>{log.msg}</span>
</motion.div>
@@ -103,11 +102,11 @@ export default function LiveFeed() {
{/* Mobile View: Vertical Stack */}
<div className="md:hidden flex flex-col gap-2 w-full p-4 h-full overflow-hidden">
{logs.map((log) => (
<div key={log.id} className="flex gap-2 w-full truncate border-b border-zinc-900/50 pb-1 last:border-0">
<span className="text-zinc-700 w-16 shrink-0">{log.time.split('.')[0]}</span>
<span className={`font-bold w-8 shrink-0 ${log.type === 'RISK' ? 'text-blue-400' :
<div key={log.id} className="flex gap-2 w-full truncate border-b border-[rgba(26,24,19,0.10)] pb-1 last:border-0">
<span className="text-nofx-text-muted w-16 shrink-0">{log.time.split('.')[0]}</span>
<span className={`font-bold w-8 shrink-0 ${log.type === 'RISK' ? 'text-nofx-danger' :
log.type === 'SIGNAL' ? 'text-nofx-gold' :
'text-zinc-500'
'text-nofx-text-muted'
}`}>{log.type}</span>
<span className={`${log.color} truncate`}>{log.msg}</span>
</div>

View File

@@ -83,8 +83,8 @@ export default function TerminalHero() {
{/* BACKGROUND LAYERS */}
{/* 1. Grid */}
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 mix-blend-soft-light pointer-events-none"></div>
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none md:hidden" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(100px) scale(2)' }}></div>
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-10 mix-blend-multiply pointer-events-none"></div>
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-[linear-gradient(to_right,#1a181310_1px,transparent_1px),linear-gradient(to_bottom,#1a181310_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] pointer-events-none md:hidden" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(100px) scale(2)' }}></div>
<div className="absolute inset-0 bg-grid-pattern opacity-[0.03] pointer-events-none"></div>
{/* 2. World Map / Data Viz Background (Abstract) */}
@@ -94,8 +94,8 @@ export default function TerminalHero() {
</div>
{/* 3. Gradient Spots - Intensified for Mobile */}
<div className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-nofx-gold/20 rounded-full blur-[120px] pointer-events-none mix-blend-screen"></div>
<div className="absolute bottom-[-10%] right-[-10%] w-[40vw] h-[40vw] bg-nofx-accent/10 rounded-full blur-[120px] pointer-events-none mix-blend-screen"></div>
<div className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-nofx-gold/10 rounded-full blur-[120px] pointer-events-none"></div>
<div className="absolute bottom-[-10%] right-[-10%] w-[40vw] h-[40vw] bg-nofx-accent/10 rounded-full blur-[120px] pointer-events-none"></div>
{/* Mobile Bottom Fade */}
<div className="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-t from-nofx-bg to-transparent z-20 pointer-events-none md:hidden" />
@@ -111,20 +111,20 @@ export default function TerminalHero() {
<div className="relative z-10 flex-1 grid grid-cols-1 lg:grid-cols-12 gap-0 lg:gap-8 max-w-[1800px] mx-auto w-full px-6 h-full pb-20 pt-10 pointer-events-none">
{/* LEFT COLUMN: TELEMETRY & STATUS */}
<div className="hidden lg:flex col-span-3 flex-col justify-between h-full border-r border-white/5 pr-8 py-10 pointer-events-auto">
<div className="hidden lg:flex col-span-3 flex-col justify-between h-full border-r border-[rgba(26,24,19,0.14)] pr-8 py-10 pointer-events-auto">
{/* Top: System Health */}
<div className="space-y-6">
<div className="tech-border p-4 bg-black/40 backdrop-blur-sm">
<div className="border border-[rgba(26,24,19,0.14)] rounded p-4 bg-nofx-bg-lighter">
<h3 className="text-xs font-mono text-nofx-gold mb-4 flex items-center gap-2">
<Activity className="w-3 h-3" /> SYSTEM_DIAGNOSTICS
</h3>
<div className="space-y-3 font-mono text-[10px] text-zinc-400">
<div className="space-y-3 font-mono text-[10px] text-nofx-text-muted">
<div className="flex justify-between items-center">
<span>KERNEL_LATENCY</span>
<span className="text-nofx-accent">12ms</span>
</div>
<div className="w-full h-1 bg-zinc-800 rounded-full overflow-hidden">
<div className="w-full h-1 bg-nofx-bg-deeper rounded-full overflow-hidden">
<div className="w-[90%] h-full bg-nofx-accent/50"></div>
</div>
@@ -132,19 +132,19 @@ export default function TerminalHero() {
<span>MEMORY_INTEGRITY</span>
<span className="text-nofx-success">100%</span>
</div>
<div className="w-full h-1 bg-zinc-800 rounded-full overflow-hidden">
<div className="w-full h-1 bg-nofx-bg-deeper rounded-full overflow-hidden">
<div className="w-full h-full bg-nofx-success/50"></div>
</div>
<div className="flex justify-between items-center">
<span>UPTIME</span>
<span className="text-white">99.999%</span>
<span className="text-nofx-text">99.999%</span>
</div>
</div>
</div>
<div className="p-4 border border-zinc-800/50 rounded bg-zinc-900/20">
<div className="flex items-center gap-3 text-zinc-500 mb-2">
<div className="p-4 border border-[rgba(26,24,19,0.14)] rounded bg-nofx-bg-lighter">
<div className="flex items-center gap-3 text-nofx-text-muted mb-2">
<Shield className="w-4 h-4" />
<span className="text-[10px] font-mono tracking-widest">SECURITY PROTOCOLS</span>
</div>
@@ -152,14 +152,14 @@ export default function TerminalHero() {
<div className="h-1 flex-1 bg-nofx-gold"></div>
<div className="h-1 flex-1 bg-nofx-gold"></div>
<div className="h-1 flex-1 bg-nofx-gold"></div>
<div className="h-1 flex-1 bg-zinc-800"></div>
<div className="h-1 flex-1 bg-nofx-bg-deeper"></div>
</div>
<div className="mt-2 text-right text-[10px] text-nofx-gold/80 font-mono">LEVEL 3 ACTIVATE</div>
</div>
</div>
{/* Bottom: Network Log */}
<div className="font-mono text-[10px] text-zinc-600 space-y-1 opacity-70">
<div className="font-mono text-[10px] text-nofx-text-muted space-y-1 opacity-70">
<div>&gt; CONNECTING TO MARKET DATA... OK</div>
<div>&gt; SYNCING VENUES (424/424)... OK</div>
<div>&gt; LOADING MULTI-ASSET UNIVERSE... DONE</div>
@@ -185,13 +185,13 @@ export default function TerminalHero() {
{/* Main Title - Massive & Impactful */}
{/* Main Title - Massive & Impactful */}
<div className="relative z-20 mix-blend-hard-light md:mix-blend-normal">
<h1 className="text-5xl sm:text-6xl md:text-8xl lg:text-9xl font-black tracking-tighter leading-[0.9] md:leading-[0.8] mb-6 select-none bg-clip-text text-transparent bg-gradient-to-b from-white via-white to-zinc-600 drop-shadow-2xl">
<div className="relative z-20">
<h1 className="text-5xl sm:text-6xl md:text-8xl lg:text-9xl font-black tracking-tighter leading-[0.9] md:leading-[0.8] mb-6 select-none text-nofx-text">
AGENTIC<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold via-white to-nofx-gold animate-shimmer bg-[length:200%_auto] tracking-tight filter drop-shadow-[0_0_15px_rgba(234,179,8,0.3)]">TRADING</span>
<span className="text-nofx-gold animate-shimmer tracking-tight">TRADING</span>
</h1>
<p className="max-w-xl text-zinc-200 md:text-zinc-400 text-lg mb-6 font-light leading-relaxed drop-shadow-md">
<p className="max-w-xl text-nofx-text-muted text-lg mb-6 font-light leading-relaxed">
Professional AI trading agents for US stocks, commodities, FX and Pre-IPO synthetic markets.
Build institutional-grade strategies by chatting in plain English.
</p>
@@ -210,10 +210,10 @@ export default function TerminalHero() {
<div className="flex flex-wrap gap-4 font-mono">
{['US STOCKS', 'COMMODITIES', 'FOREX', 'PRE-IPO'].map((market) => (
<div key={market} className="relative group cursor-default">
<div className="absolute -inset-0.5 bg-gradient-to-r from-nofx-gold/20 to-blue-600/20 rounded-lg blur opacity-0 group-hover:opacity-100 transition duration-500"></div>
<div className="relative flex items-center gap-3 px-6 py-3 rounded-lg bg-zinc-900/80 border border-zinc-700 hover:border-nofx-gold/50 transition-all duration-300 backdrop-blur-sm">
<div className="w-1.5 h-1.5 rounded-full bg-nofx-success shadow-[0_0_8px_rgba(74,222,128,0.6)] animate-pulse"></div>
<span className="text-lg md:text-xl font-bold text-white tracking-wider group-hover:text-nofx-gold transition-colors">{market}</span>
<div className="absolute -inset-0.5 bg-nofx-gold/15 rounded-lg blur opacity-0 group-hover:opacity-100 transition duration-500"></div>
<div className="relative flex items-center gap-3 px-6 py-3 rounded-lg bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] hover:border-nofx-gold/50 transition-all duration-300">
<div className="w-1.5 h-1.5 rounded-full bg-nofx-success animate-pulse"></div>
<span className="text-lg md:text-xl font-bold text-nofx-text tracking-wider group-hover:text-nofx-gold transition-colors">{market}</span>
</div>
</div>
))}
@@ -221,10 +221,10 @@ export default function TerminalHero() {
</div>
{/* Command Line Input Simulation */}
<div className="w-full max-w-lg h-12 bg-black/50 border border-zinc-800 rounded flex items-center px-4 mb-10 font-mono text-sm shadow-2xl backdrop-blur-sm group hover:border-nofx-gold/50 transition-colors cursor-text" onClick={() => document.getElementById('market-scanner')?.scrollIntoView({ behavior: 'smooth' })}>
<div className="w-full max-w-lg h-12 bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] rounded flex items-center px-4 mb-10 font-mono text-sm shadow-sm group hover:border-nofx-gold/50 transition-colors cursor-text" onClick={() => document.getElementById('market-scanner')?.scrollIntoView({ behavior: 'smooth' })}>
<span className="text-nofx-success mr-2"></span>
<span className="text-nofx-accent mr-2">~</span>
<span className="text-zinc-500">create US stock trader --idea="breakouts"</span>
<span className="text-nofx-text-muted">create US stock trader --idea="breakouts"</span>
<span className="w-2 h-4 bg-nofx-gold ml-1 animate-pulse"></span>
</div>
@@ -232,13 +232,13 @@ export default function TerminalHero() {
<div className="flex flex-col sm:flex-row gap-4 w-full justify-center">
<button
onClick={() => document.getElementById('market-scanner')?.scrollIntoView({ behavior: 'smooth' })}
className="group relative overflow-hidden bg-nofx-gold text-black px-8 py-4 font-bold font-mono tracking-wider hover:scale-105 transition-transform duration-200"
className="group relative overflow-hidden bg-nofx-gold text-nofx-bg-lighter px-8 py-4 font-bold font-mono tracking-wider hover:scale-105 transition-transform duration-200"
style={{ clipPath: 'polygon(10% 0, 100% 0, 100% 70%, 90% 100%, 0 100%, 0 30%)' }}
>
<span className="relative z-10 flex items-center gap-2">
CREATE STOCK TRADER <ArrowRight className="w-4 h-4" />
</span>
<div className="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
<div className="absolute inset-0 bg-nofx-text/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div>
</button>
</div>
@@ -251,13 +251,13 @@ export default function TerminalHero() {
{/* RIGHT COLUMN: Trader Terminal - Desktop Only */}
<div className="absolute top-0 right-0 h-full w-[50vw] hidden lg:flex flex-col items-end justify-end pr-8 pb-20 z-10">
{/* Subtle gradient orb */}
<div className="absolute top-1/2 right-[10%] -translate-y-1/2 w-[400px] h-[400px] rounded-full bg-gradient-to-br from-nofx-gold/10 via-nofx-gold/5 to-transparent blur-[100px] pointer-events-none"></div>
<div className="absolute top-1/2 right-[10%] -translate-y-1/2 w-[400px] h-[400px] rounded-full bg-nofx-gold/8 blur-[100px] pointer-events-none"></div>
{/* Subtle grid fade */}
<div
className="absolute inset-0 opacity-[0.03] pointer-events-none"
style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.3) 1px, transparent 0)',
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(26,24,19,0.3) 1px, transparent 0)',
backgroundSize: '40px 40px',
maskImage: 'radial-gradient(ellipse 80% 80% at 70% 50%, black 20%, transparent 70%)',
WebkitMaskImage: 'radial-gradient(ellipse 80% 80% at 70% 50%, black 20%, transparent 70%)'
@@ -271,11 +271,11 @@ export default function TerminalHero() {
</div>
{/* FLOATING TICKER FOOTER */}
<div className="absolute bottom-0 w-full bg-black/80 border-t border-zinc-800/50 backdrop-blur-md z-30 overflow-hidden py-2 flex items-center">
<div className="flex animate-marquee whitespace-nowrap gap-12 text-xs font-mono text-zinc-500 px-4">
<span className="flex items-center gap-2"><Globe className="w-3 h-3 text-zinc-600" /> GLOBAL MARKET ACCESS</span>
<div className="absolute bottom-0 w-full bg-nofx-bg-lighter border-t border-[rgba(26,24,19,0.14)] backdrop-blur-md z-30 overflow-hidden py-2 flex items-center">
<div className="flex animate-marquee whitespace-nowrap gap-12 text-xs font-mono text-nofx-text-muted px-4">
<span className="flex items-center gap-2"><Globe className="w-3 h-3 text-nofx-text-muted" /> GLOBAL MARKET ACCESS</span>
<span className="flex items-center gap-2 text-nofx-gold"><Zap className="w-3 h-3" /> MULTI-ASSET ROUTING ENABLED</span>
<span className="flex items-center gap-2"><Wifi className="w-3 h-3 text-green-500" /> LOW LATENCY LINK: 12ms</span>
<span className="flex items-center gap-2"><Wifi className="w-3 h-3 text-nofx-success" /> LOW LATENCY LINK: 12ms</span>
{/* Dynamic Coins */}
{Object.entries(prices).map(([symbol, price]) => (
@@ -295,8 +295,6 @@ export default function TerminalHero() {
</div>
</div>
{/* CRT OVERLAY (Global) */}
<div className="absolute inset-0 crt-overlay pointer-events-none z-50 opacity-40"></div>
</section >
)
}
@@ -311,28 +309,28 @@ function CommunityStats() {
label: 'GITHUB STARS',
value: isLoading ? '...' : (error ? '10,500+' : stars.toLocaleString()),
icon: Star,
color: 'text-yellow-400',
color: 'text-nofx-gold',
href: OFFICIAL_LINKS.github
},
{
label: 'FORKS',
value: isLoading ? '...' : (error ? '2,800+' : forks.toLocaleString()),
icon: GitFork,
color: 'text-blue-400',
color: 'text-nofx-accent',
href: `${OFFICIAL_LINKS.github}/fork`
},
{
label: 'CONTRIBUTORS',
value: isLoading ? '...' : (contributors > 0 ? contributors : '50+'),
icon: Users,
color: 'text-green-400',
color: 'text-nofx-success',
href: `${OFFICIAL_LINKS.github}/graphs/contributors`
},
{
label: 'DEV COMMUNITY',
value: '6,600+',
icon: MessageCircle,
color: 'text-blue-500',
color: 'text-nofx-accent',
href: OFFICIAL_LINKS.telegram
}
]
@@ -345,13 +343,13 @@ function CommunityStats() {
href={stat.href}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center justify-center p-3 rounded bg-black/40 border border-zinc-800/50 backdrop-blur-sm group hover:border-nofx-gold/30 transition-all cursor-pointer hover:bg-white/5"
className="flex flex-col items-center justify-center p-3 rounded bg-nofx-bg-lighter border border-[rgba(26,24,19,0.14)] group hover:border-nofx-gold/30 transition-all cursor-pointer hover:bg-nofx-bg-deeper"
>
<div className="flex items-center gap-2 mb-1">
<stat.icon className={`w-4 h-4 ${stat.color}`} />
<span className="text-[10px] font-mono text-zinc-500 tracking-wider">{stat.label}</span>
<span className="text-[10px] font-mono text-nofx-text-muted tracking-wider">{stat.label}</span>
</div>
<span className="text-xl font-bold font-mono text-white group-hover:text-nofx-gold transition-colors">{stat.value}</span>
<span className="text-xl font-bold font-mono text-nofx-text group-hover:text-nofx-gold transition-colors">{stat.value}</span>
</a>
))}
</div>

View File

@@ -9,17 +9,17 @@ import { LanguageSwitcher } from '../common/LanguageSwitcher'
const labels = {
zh: {
welcome: '欢迎使用 NOFX',
subtitle: '创建账号开始使用',
email: '邮箱',
welcome: 'Welcome to NOFX',
subtitle: 'Create your account to get started',
email: 'Email',
emailPlaceholder: 'you@example.com',
password: '密码',
passwordPlaceholder: '至少 8 个字符',
passwordError: '密码至少需要 8 个字符',
submit: '开始使用',
submitting: '创建中...',
setupFailed: '创建失败,请重试',
singleUser: '单用户系统 — 这是唯一的账号',
password: 'Password',
passwordPlaceholder: 'At least 8 characters',
passwordError: 'Password must be at least 8 characters',
submit: 'Get Started',
submitting: 'Creating account...',
setupFailed: 'Setup failed, please try again',
singleUser: 'Single-user system — this is the only account',
},
en: {
welcome: 'Welcome to NOFX',
@@ -88,7 +88,7 @@ export function SetupPage() {
}
return (
<div className="relative min-h-screen w-full overflow-hidden bg-[#0a0a0f]">
<div className="relative min-h-screen w-full overflow-hidden bg-nofx-bg">
{/* Decorative background - simulates the main app behind a modal */}
{/* Grid */}
@@ -99,33 +99,33 @@ export function SetupPage() {
{/* Glow spots */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-[10%] left-[15%] w-[500px] h-[500px] bg-nofx-gold/8 rounded-full blur-[150px]" />
<div className="absolute bottom-[5%] right-[10%] w-[400px] h-[400px] bg-indigo-500/6 rounded-full blur-[140px]" />
<div className="absolute top-[40%] right-[30%] w-[300px] h-[300px] bg-emerald-500/4 rounded-full blur-[120px]" />
<div className="absolute bottom-[5%] right-[10%] w-[400px] h-[400px] bg-nofx-gold/6 rounded-full blur-[140px]" />
<div className="absolute top-[40%] right-[30%] w-[300px] h-[300px] bg-nofx-success/4 rounded-full blur-[120px]" />
</div>
{/* Faux UI elements in background to simulate the app */}
<div className="absolute inset-0 pointer-events-none opacity-[0.06]">
{/* Fake header bar */}
<div className="h-14 border-b border-white/20 flex items-center px-6 gap-4">
<div className="w-8 h-8 rounded-lg bg-white/40" />
<div className="h-3 w-20 rounded bg-white/30" />
<div className="h-3 w-16 rounded bg-white/20 ml-4" />
<div className="h-3 w-16 rounded bg-white/20" />
<div className="h-3 w-16 rounded bg-white/20" />
<div className="h-14 border-b border-nofx-text/20 flex items-center px-6 gap-4">
<div className="w-8 h-8 rounded-lg bg-nofx-text/40" />
<div className="h-3 w-20 rounded bg-nofx-text/30" />
<div className="h-3 w-16 rounded bg-nofx-text/20 ml-4" />
<div className="h-3 w-16 rounded bg-nofx-text/20" />
<div className="h-3 w-16 rounded bg-nofx-text/20" />
</div>
{/* Fake content cards */}
<div className="p-6 grid grid-cols-4 gap-4 mt-2">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 rounded-xl border border-white/15 bg-white/5" />
<div key={i} className="h-24 rounded-xl border border-nofx-text/15 bg-nofx-text/5" />
))}
</div>
<div className="px-6 mt-2">
<div className="h-64 rounded-xl border border-white/15 bg-white/5" />
<div className="h-64 rounded-xl border border-nofx-text/15 bg-nofx-text/5" />
</div>
</div>
{/* Blur overlay */}
<div className="absolute inset-0 backdrop-blur-md bg-black/60" />
<div className="absolute inset-0 backdrop-blur-md bg-nofx-bg/60" />
<LanguageSwitcher />
@@ -138,25 +138,25 @@ export function SetupPage() {
<div className="flex justify-center mb-4">
<div className="relative">
<div className="absolute -inset-4 bg-nofx-gold/20 rounded-full blur-2xl" />
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10 drop-shadow-[0_0_15px_rgba(240,185,11,0.3)]" />
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10" />
</div>
</div>
<h1 className="text-2xl font-bold text-white mb-1.5">{l.welcome}</h1>
<p className="text-zinc-500 text-sm">{l.subtitle}</p>
<h1 className="text-2xl font-bold text-nofx-text mb-1.5">{l.welcome}</h1>
<p className="text-nofx-text-muted text-sm">{l.subtitle}</p>
</div>
{/* Card */}
<div className="bg-zinc-900/80 backdrop-blur-2xl border border-white/10 rounded-2xl p-8 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.5),0_0_40px_-10px_rgba(240,185,11,0.08)]">
<div className="bg-nofx-bg-lighter backdrop-blur-2xl border border-[rgba(26,24,19,0.14)] rounded-2xl p-8 shadow-lg">
<form onSubmit={handleSubmit} className="space-y-5">
{/* Email */}
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.email}</label>
<label className="block text-xs font-medium text-nofx-text-muted mb-2">{l.email}</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-xl px-4 py-3 text-sm text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
placeholder={l.emailPlaceholder}
required
autoFocus
@@ -165,20 +165,20 @@ export function SetupPage() {
{/* Password */}
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.password}</label>
<label className="block text-xs font-medium text-nofx-text-muted mb-2">{l.password}</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 pr-11 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-xl px-4 py-3 pr-11 text-sm text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
placeholder={l.passwordPlaceholder}
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
@@ -193,7 +193,7 @@ export function SetupPage() {
{/* Error */}
{error && (
<p className="text-xs text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
<p className="text-xs text-nofx-danger bg-nofx-danger/10 border border-nofx-danger/20 rounded-lg px-3 py-2">
{error}
</p>
)}
@@ -202,14 +202,14 @@ export function SetupPage() {
<button
type="submit"
disabled={loading}
className="w-full bg-nofx-gold hover:bg-yellow-400 active:scale-[0.98] text-black font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2 shadow-[0_0_20px_rgba(240,185,11,0.2)]"
className="w-full bg-nofx-gold hover:bg-nofx-gold-highlight active:scale-[0.98] text-nofx-bg font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2"
>
{loading ? l.submitting : l.submit}
</button>
</form>
</div>
<p className="text-center text-xs text-zinc-600 mt-6">
<p className="text-center text-xs text-nofx-text-muted mt-6">
{l.singleUser}
</p>
</div>

View File

@@ -92,11 +92,11 @@ export function TwoStageKeyModal({
setProcessing(true)
try {
// 生成混淆字符串
// Generate obfuscation string
const obfuscation = generateObfuscation()
setManualObfuscationValue(obfuscation)
// 尝试复制到剪贴板
// Try to copy to clipboard
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(obfuscation)
@@ -105,14 +105,14 @@ export function TwoStageKeyModal({
...obfuscationLog,
`Stage 1: ${new Date().toISOString()} - Auto copied obfuscation`,
])
toast.success('已复制混淆字符串到剪贴板')
toast.success('Obfuscation string copied to clipboard')
} catch {
setClipboardStatus('failed')
setObfuscationLog([
...obfuscationLog,
`Stage 1: ${new Date().toISOString()} - Auto copy failed, manual required`,
])
toast.error('复制失败,请手动复制混淆字符串')
toast.error('Copy failed, please copy the obfuscation string manually')
}
} else {
setClipboardStatus('failed')
@@ -120,7 +120,7 @@ export function TwoStageKeyModal({
...obfuscationLog,
`Stage 1: ${new Date().toISOString()} - Clipboard API not available`,
])
toast('当前浏览器不支持自动复制,请手动复制')
toast('This browser does not support automatic copy, please copy manually')
}
setTimeout(() => {
@@ -179,17 +179,17 @@ export function TwoStageKeyModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<div className="bg-gray-900 p-8 rounded-xl max-w-lg w-full mx-4 border border-gray-700">
<div className="bg-nofx-bg-lighter p-8 rounded-xl max-w-lg w-full mx-4 border border-[rgba(26,24,19,0.14)]">
<div className="text-center mb-6">
<h2 className="text-xl font-bold text-white mb-2">
<h2 className="text-xl font-bold text-nofx-text mb-2">
🔐 {t('twoStageKey.title', language)}
{contextLabel && (
<span className="text-gray-300 text-base font-normal ml-2">
<span className="text-nofx-text-muted text-base font-normal ml-2">
({contextLabel})
</span>
)}
</h2>
<p className="text-gray-300 text-sm">
<p className="text-nofx-text-muted text-sm">
{stage === 1
? t('twoStageKey.stage1Description', language, {
length: expectedPart1Length,
@@ -208,7 +208,7 @@ export function TwoStageKeyModal({
{stage === 1 && (
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">
<label className="block text-nofx-text-muted text-sm mb-2">
{t('twoStageKey.stage1InputLabel', language)} (
{expectedPart1Length} {t('twoStageKey.characters', language)})
</label>
@@ -218,13 +218,13 @@ export function TwoStageKeyModal({
value={part1}
onChange={(e) => setPart1(e.target.value)}
placeholder="0x1234..."
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white font-mono text-sm focus:border-blue-500 focus:outline-none"
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-3 text-nofx-text font-mono text-sm focus:border-nofx-gold focus:outline-none"
maxLength={expectedPart1Length + 2} // +2 for optional 0x prefix
disabled={processing}
/>
</div>
{error && <div className="text-red-400 text-sm">{error}</div>}
{error && <div className="text-nofx-danger text-sm">{error}</div>}
<div className="flex gap-3">
<button
@@ -233,7 +233,7 @@ export function TwoStageKeyModal({
(part1.startsWith('0x') ? part1.slice(2) : part1).length <
expectedPart1Length || processing
}
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors"
className="flex-1 bg-nofx-gold hover:bg-nofx-gold-highlight disabled:bg-nofx-bg-deeper disabled:text-nofx-text-muted text-nofx-bg font-medium py-3 px-4 rounded-lg transition-colors"
>
{processing
? t('twoStageKey.processing', language)
@@ -242,7 +242,7 @@ export function TwoStageKeyModal({
<button
onClick={onCancel}
disabled={processing}
className="px-6 py-3 text-gray-300 hover:text-white border border-gray-600 rounded-lg transition-colors"
className="px-6 py-3 text-nofx-text-muted hover:text-nofx-text border border-[rgba(26,24,19,0.14)] rounded-lg transition-colors"
>
{t('twoStageKey.cancelButton', language)}
</button>
@@ -252,9 +252,9 @@ export function TwoStageKeyModal({
{/* Transition Message */}
{stage === 2 && clipboardStatus !== 'idle' && (
<div className="mb-4 p-4 rounded-lg bg-blue-900/50 border border-blue-600">
<div className="mb-4 p-4 rounded-lg bg-nofx-gold/10 border border-nofx-gold/40">
{clipboardStatus === 'copied' && (
<div className="text-blue-300">
<div className="text-nofx-text">
<div className="font-medium">
{t('twoStageKey.obfuscationCopied', language)}
</div>
@@ -264,11 +264,11 @@ export function TwoStageKeyModal({
</div>
)}
{clipboardStatus === 'failed' && manualObfuscationValue && (
<div className="text-yellow-300">
<div className="text-nofx-gold">
<div className="font-medium">
{t('twoStageKey.obfuscationManual', language)}
</div>
<div className="text-xs mt-2 p-2 bg-gray-800 rounded font-mono break-all border">
<div className="text-xs mt-2 p-2 bg-nofx-bg-deeper rounded font-mono break-all border border-[rgba(26,24,19,0.14)]">
{manualObfuscationValue}
</div>
<div className="text-sm mt-1">
@@ -283,7 +283,7 @@ export function TwoStageKeyModal({
{stage === 2 && (
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">
<label className="block text-nofx-text-muted text-sm mb-2">
{t('twoStageKey.stage2InputLabel', language)} (
{expectedPart2Length} {t('twoStageKey.characters', language)})
</label>
@@ -293,12 +293,12 @@ export function TwoStageKeyModal({
value={part2}
onChange={(e) => setPart2(e.target.value)}
placeholder="...5678"
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white font-mono text-sm focus:border-blue-500 focus:outline-none"
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-3 text-nofx-text font-mono text-sm focus:border-nofx-gold focus:outline-none"
maxLength={expectedPart2Length + 2}
/>
</div>
{error && <div className="text-red-400 text-sm">{error}</div>}
{error && <div className="text-nofx-danger text-sm">{error}</div>}
<div className="flex gap-3">
<button
@@ -307,13 +307,13 @@ export function TwoStageKeyModal({
(part2.startsWith('0x') ? part2.slice(2) : part2).length <
expectedPart2Length
}
className="flex-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors"
className="flex-1 bg-nofx-success hover:bg-nofx-success/90 disabled:bg-nofx-bg-deeper disabled:text-nofx-text-muted text-nofx-bg font-medium py-3 px-4 rounded-lg transition-colors"
>
🔒 {t('twoStageKey.encryptButton', language)}
</button>
<button
onClick={handleReset}
className="px-6 py-3 text-gray-300 hover:text-white border border-gray-600 rounded-lg transition-colors"
className="px-6 py-3 text-nofx-text-muted hover:text-nofx-text border border-[rgba(26,24,19,0.14)] rounded-lg transition-colors"
>
{t('twoStageKey.backButton', language)}
</button>

View File

@@ -50,29 +50,29 @@ export function GridRiskPanel({
const getRegimeColor = (regime: string) => {
switch (regime) {
case 'narrow': return '#0ECB81'
case 'standard': return '#F0B90B'
case 'wide': return '#F7931A'
case 'volatile': return '#F6465D'
case 'trending': return '#8B5CF6'
default: return '#848E9C'
case 'narrow': return '#2E8B57'
case 'standard': return '#E0483B'
case 'wide': return '#E0483B'
case 'volatile': return '#D6433A'
case 'trending': return '#E0483B'
default: return '#8A8478'
}
}
const getBreakoutColor = (level: string) => {
switch (level) {
case 'none': return '#0ECB81'
case 'short': return '#F0B90B'
case 'mid': return '#F7931A'
case 'long': return '#F6465D'
default: return '#848E9C'
case 'none': return '#2E8B57'
case 'short': return '#E0483B'
case 'mid': return '#E0483B'
case 'long': return '#D6433A'
default: return '#8A8478'
}
}
const getPositionColor = (percent: number) => {
if (percent < 50) return '#0ECB81'
if (percent < 80) return '#F0B90B'
return '#F6465D'
if (percent < 50) return '#2E8B57'
if (percent < 80) return '#E0483B'
return '#D6433A'
}
const formatPrice = (price: number) => {
@@ -87,13 +87,13 @@ export function GridRiskPanel({
}
const cardStyle = {
background: '#0B0E11',
border: '1px solid #2B3139',
background: '#F7F4EC',
border: '1px solid rgba(26,24,19,0.14)',
}
if (loading) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
<div className="p-3 text-center text-xs" style={{ color: '#8A8478' }}>
{ts(gridRisk.loading, language)}
</div>
)
@@ -101,7 +101,7 @@ export function GridRiskPanel({
if (error) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#F6465D' }}>
<div className="p-3 text-center text-xs" style={{ color: '#D6433A' }}>
{ts(gridRisk.error, language)}: {error}
</div>
)
@@ -109,7 +109,7 @@ export function GridRiskPanel({
if (!riskInfo) {
return (
<div className="p-3 text-center text-xs" style={{ color: '#848E9C' }}>
<div className="p-3 text-center text-xs" style={{ color: '#8A8478' }}>
{ts(gridRisk.noData, language)}
</div>
)
@@ -119,12 +119,12 @@ export function GridRiskPanel({
<div className="rounded-lg" style={cardStyle}>
{/* Collapsible Header */}
<div
className="flex items-center justify-between p-3 cursor-pointer hover:bg-[#1E2329] transition-colors"
className="flex items-center justify-between p-3 cursor-pointer hover:bg-[#E8E2D5] transition-colors"
onClick={() => setExpanded(!expanded)}
>
<div className="flex items-center gap-2">
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
<Shield className="w-4 h-4" style={{ color: '#E0483B' }} />
<span className="font-medium text-sm" style={{ color: '#1A1813' }}>
{ts(gridRisk.gridRisk, language)}
</span>
</div>
@@ -137,7 +137,7 @@ export function GridRiskPanel({
>
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
</span>
<span className="font-mono" style={{ color: '#EAECEF' }}>
<span className="font-mono" style={{ color: '#1A1813' }}>
{riskInfo.effective_leverage.toFixed(1)}x
</span>
<span
@@ -148,9 +148,9 @@ export function GridRiskPanel({
</span>
</div>
{expanded ? (
<ChevronUp className="w-4 h-4" style={{ color: '#848E9C' }} />
<ChevronUp className="w-4 h-4" style={{ color: '#8A8478' }} />
) : (
<ChevronDown className="w-4 h-4" style={{ color: '#848E9C' }} />
<ChevronDown className="w-4 h-4" style={{ color: '#8A8478' }} />
)}
</div>
</div>
@@ -161,25 +161,25 @@ export function GridRiskPanel({
{/* Row 1: Leverage & Position */}
<div className="grid grid-cols-2 gap-3">
{/* Leverage */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<TrendingUp className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.leverageInfo, language)}</span>
<TrendingUp className="w-3 h-3" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.leverageInfo, language)}</span>
</div>
<div className="grid grid-cols-3 gap-1 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentLeverage, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{riskInfo.current_leverage}x</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.currentLeverage, language)}</div>
<div className="font-mono" style={{ color: '#1A1813' }}>{riskInfo.current_leverage}x</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.effectiveLeverage, language)}</div>
<div className="font-mono" style={{ color: '#F0B90B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.effectiveLeverage, language)}</div>
<div className="font-mono" style={{ color: '#E0483B' }}>{riskInfo.effective_leverage.toFixed(2)}x</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.recommendedLeverage, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.recommendedLeverage, language)}</div>
<div
className="font-mono"
style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#F6465D' : '#0ECB81' }}
style={{ color: riskInfo.current_leverage > riskInfo.recommended_leverage ? '#D6433A' : '#2E8B57' }}
>
{riskInfo.recommended_leverage}x
</div>
@@ -188,29 +188,29 @@ export function GridRiskPanel({
</div>
{/* Position */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<Activity className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.positionInfo, language)}</span>
<Activity className="w-3 h-3" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.positionInfo, language)}</span>
</div>
<div className="grid grid-cols-3 gap-1 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPosition, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.current_position)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.currentPosition, language)}</div>
<div className="font-mono" style={{ color: '#1A1813' }}>{formatUSD(riskInfo.current_position)}</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.maxPosition, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatUSD(riskInfo.max_position)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.maxPosition, language)}</div>
<div className="font-mono" style={{ color: '#1A1813' }}>{formatUSD(riskInfo.max_position)}</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.positionPercent, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.positionPercent, language)}</div>
<div className="font-mono" style={{ color: getPositionColor(riskInfo.position_percent) }}>
{riskInfo.position_percent.toFixed(1)}%
</div>
</div>
</div>
{/* Mini progress bar */}
<div className="h-1 mt-2 rounded-full overflow-hidden" style={{ background: '#2B3139' }}>
<div className="h-1 mt-2 rounded-full overflow-hidden" style={{ background: '#E8E2D5' }}>
<div
className="h-full rounded-full"
style={{ width: `${Math.min(riskInfo.position_percent, 100)}%`, background: getPositionColor(riskInfo.position_percent) }}
@@ -222,33 +222,33 @@ export function GridRiskPanel({
{/* Row 2: Market State & Liquidation */}
<div className="grid grid-cols-2 gap-3">
{/* Market State */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<Shield className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.marketState, language)}</span>
<Shield className="w-3 h-3" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.marketState, language)}</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.regimeLevel, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.regimeLevel, language)}</div>
<div className="font-medium" style={{ color: getRegimeColor(riskInfo.regime_level) }}>
{ts(gridRisk[(riskInfo.regime_level || 'standard') as keyof typeof gridRisk], language)}
</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.currentPrice, language)}</div>
<div className="font-mono" style={{ color: '#EAECEF' }}>{formatPrice(riskInfo.current_price)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.currentPrice, language)}</div>
<div className="font-mono" style={{ color: '#1A1813' }}>{formatPrice(riskInfo.current_price)}</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutLevel, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.breakoutLevel, language)}</div>
<div className="font-medium" style={{ color: getBreakoutColor(riskInfo.breakout_level) }}>
{ts(gridRisk[(riskInfo.breakout_level || 'none') as keyof typeof gridRisk], language)}
</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.breakoutDirection, language)}</div>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.breakoutDirection, language)}</div>
<div
className="font-medium"
style={{ color: riskInfo.breakout_direction === 'up' ? '#0ECB81' : riskInfo.breakout_direction === 'down' ? '#F6465D' : '#848E9C' }}
style={{ color: riskInfo.breakout_direction === 'up' ? '#2E8B57' : riskInfo.breakout_direction === 'down' ? '#D6433A' : '#8A8478' }}
>
{riskInfo.breakout_direction ? ts(gridRisk[riskInfo.breakout_direction as keyof typeof gridRisk], language) : '-'}
</div>
@@ -257,21 +257,21 @@ export function GridRiskPanel({
</div>
{/* Liquidation */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<AlertTriangle className="w-3 h-3" style={{ color: '#F6465D' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.liquidationInfo, language)}</span>
<AlertTriangle className="w-3 h-3" style={{ color: '#D6433A' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.liquidationInfo, language)}</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationPrice, language)}</div>
<div className="font-mono" style={{ color: '#F6465D' }}>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.liquidationPrice, language)}</div>
<div className="font-mono" style={{ color: '#D6433A' }}>
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
</div>
</div>
<div>
<div style={{ color: '#5E6673' }}>{ts(gridRisk.liquidationDistance, language)}</div>
<div className="font-mono" style={{ color: '#F6465D' }}>
<div style={{ color: '#8A8478' }}>{ts(gridRisk.liquidationDistance, language)}</div>
<div className="font-mono" style={{ color: '#D6433A' }}>
{riskInfo.liquidation_distance > 0 ? `${riskInfo.liquidation_distance.toFixed(1)}%` : '-'}
</div>
</div>
@@ -280,27 +280,27 @@ export function GridRiskPanel({
</div>
{/* Row 3: Box State */}
<div className="p-2 rounded" style={{ background: '#1E2329' }}>
<div className="p-2 rounded" style={{ background: '#E8E2D5' }}>
<div className="flex items-center gap-1 mb-2">
<Box className="w-3 h-3" style={{ color: '#F0B90B' }} />
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{ts(gridRisk.boxState, language)}</span>
<Box className="w-3 h-3" style={{ color: '#E0483B' }} />
<span className="text-xs font-medium" style={{ color: '#8A8478' }}>{ts(gridRisk.boxState, language)}</span>
</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{ts(gridRisk.shortBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}>
<span style={{ color: '#8A8478' }}>{ts(gridRisk.shortBox, language)}</span>
<span className="font-mono" style={{ color: '#1A1813' }}>
{formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
</span>
</div>
<div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{ts(gridRisk.midBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}>
<span style={{ color: '#8A8478' }}>{ts(gridRisk.midBox, language)}</span>
<span className="font-mono" style={{ color: '#1A1813' }}>
{formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
</span>
</div>
<div className="flex justify-between">
<span style={{ color: '#5E6673' }}>{ts(gridRisk.longBox, language)}</span>
<span className="font-mono" style={{ color: '#EAECEF' }}>
<span style={{ color: '#8A8478' }}>{ts(gridRisk.longBox, language)}</span>
<span className="font-mono" style={{ color: '#1A1813' }}>
{formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
</span>
</div>

View File

@@ -0,0 +1,75 @@
import { useMemo } from 'react'
import type { Kline } from '../../lib/api/data'
interface CandlesProps {
data: Kline[]
width?: number
height?: number
/** stretch to fill the parent's height (parent must have a definite height) */
fill?: boolean
}
/**
* Candles renders a compact OHLC candlestick chart from real kline data
* (GET /api/klines). Up candles use the terminal's profit green, down candles
* the loss red. Purely presentational — the parent fetches the real series.
*/
export function Candles({ data, width = 640, height = 150, fill = false }: CandlesProps) {
const candles = useMemo(() => {
if (!data || data.length === 0) return []
const slice = data.slice(-40)
const highs = slice.map((k) => k.high)
const lows = slice.map((k) => k.low)
const max = Math.max(...highs)
const min = Math.min(...lows)
const span = max - min || 1
const pad = 6
const gap = (width - pad * 2) / slice.length
const bodyW = Math.max(2, gap * 0.6)
const y = (v: number) => pad + (1 - (v - min) / span) * (height - pad * 2)
return slice.map((k, i) => {
const cx = pad + gap * i + gap / 2
const up = k.close >= k.open
return {
cx,
up,
wickTop: y(k.high),
wickBot: y(k.low),
bodyTop: y(Math.max(k.open, k.close)),
bodyBot: y(Math.min(k.open, k.close)),
bodyW,
}
})
}, [data, width, height])
if (candles.length === 0) return null
return (
<svg
width="100%"
height={fill ? '100%' : undefined}
viewBox={`0 0 ${width} ${height}`}
preserveAspectRatio={fill ? 'none' : 'xMidYMid meet'}
role="img"
aria-label="Candlestick chart"
style={{ display: 'block', ...(fill ? { height: '100%' } : {}) }}
>
{candles.map((c, i) => {
const color = c.up ? 'var(--tm-up)' : 'var(--tm-dn)'
return (
<g key={i} stroke={color} fill={color}>
<line x1={c.cx} y1={c.wickTop} x2={c.cx} y2={c.wickBot} strokeWidth={1} />
<rect
x={c.cx - c.bodyW / 2}
y={c.bodyTop}
width={c.bodyW}
height={Math.max(1, c.bodyBot - c.bodyTop)}
/>
</g>
)
})}
</svg>
)
}
export default Candles

View File

@@ -0,0 +1,300 @@
import { useMemo } from 'react'
import type { DecisionRecord } from '../../types'
/**
* ExecutionLog renders the AI trading agent's real decisions and order results
* as a high-density, newest-first terminal stream — Bloomberg-style log on the
* cream paper theme. Each cycle is a clearly-delimited block: a header bar
* (cycle no · time · action count), one badge line per AI action, and one
* indented sub-line per execution-log entry, color-coded by
* success / throttle / risk.
*
* Real data only — renders verbatim what's present on each DecisionRecord;
* verbose throttle strings are tidied for the gist but never invented.
*/
const C_AMBER = '#c8860b' // throttle / block warnings
// Strip dex prefix + quote suffix to the bare base ticker.
function baseSymbol(raw: string): string {
return raw
.toUpperCase()
.replace(/^XYZ:/, '')
.replace(/(USDT|USDC|USD)$/, '')
}
// HH:MM:SS from an ISO timestamp; guards against invalid input.
function fmtTime(iso: string): string {
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return '--:--:--'
const pad = (n: number) => String(n).padStart(2, '0')
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
type Side = 'long' | 'short' | 'flat'
// Trade side of an action token — drives badge color.
function actionSide(action: string): Side {
const a = action.toLowerCase()
if (a === 'open_long' || a === 'close_short') return 'long'
if (a === 'open_short' || a === 'close_long') return 'short'
return 'flat'
}
function sideColor(side: Side): string {
if (side === 'long') return 'var(--tm-up)'
if (side === 'short') return 'var(--tm-dn)'
return 'var(--tm-muted)'
}
type LogTone = 'ok' | 'warn' | 'risk' | 'info'
// Classify an execution-log line into a tone for color-coding.
function logTone(line: string): LogTone {
const s = line.toLowerCase()
if (s.includes('succeed') || s.includes('success') || line.includes('✓')) return 'ok'
if (s.includes('throttle') || s.includes('re-entry') || s.includes('cooldown') || s.includes('blocked'))
return 'warn'
if (
s.includes('risk') ||
s.includes('fail') ||
s.includes('reject') ||
s.includes('error') ||
s.includes('denied') ||
line.includes('✗') ||
line.includes('❌')
)
return 'risk'
return 'info'
}
const TONE_COLOR: Record<LogTone, string> = {
ok: 'var(--tm-up)',
warn: C_AMBER,
risk: 'var(--tm-dn)',
info: 'var(--tm-ink-2)',
}
const TONE_GLYPH: Record<LogTone, string> = { ok: '✓', warn: '⚠', risk: '❌', info: '·' }
/**
* Tidy a verbose execution-log string to its gist without fabricating data.
* Strips a leading "└"/glyph the backend may already include (we render our
* own), the symbol (already shown), and collapses the wordy throttle phrasing
* to "throttle · closed 28m ago, wait 2m". Falls back to the original line.
*/
function cleanLog(raw: string): string {
let s = raw.replace(/^[\s└>•·]*[✓✗⚠❌]?\s*/, '').trim()
const throttle = s.match(/closed\s+([0-9smhd.]+)\s+ago;\s*wait\s+([0-9smhd.]+)/i)
if (throttle) {
const ago = throttle[1].replace(/(\d)0s$/, '$1').replace(/0s$/, '')
const wait = throttle[2].replace(/(\d)0s$/, '$1').replace(/0s$/, '')
return `throttle · closed ${ago} ago, wait ${wait}`
}
// drop a redundant leading "SYMBOL action" prefix when present
s = s.replace(/^[A-Z0-9:_-]{2,12}\s+(open_long|open_short|close_long|close_short)\s+/i, '')
return s
}
interface ExecutionLogProps {
decisions?: DecisionRecord[]
height?: number
}
export function ExecutionLog({ decisions, height = 440 }: ExecutionLogProps) {
// Newest cycle first.
const cycles = useMemo(() => {
const list = decisions ?? []
return [...list].sort((a, b) => b.cycle_number - a.cycle_number)
}, [decisions])
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
{/* header */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 2 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Execution log</span>
<span
className="tm-sc"
style={{ marginLeft: 'auto', color: cycles.length ? 'var(--tm-up)' : 'var(--tm-muted)' }}
>
{cycles.length ? `${cycles.length} cyc` : '—'}
</span>
</div>
<div className="tm-sc" style={{ fontSize: 9, marginBottom: 5 }}>
Execution log · AI decisions & fills per cycle
</div>
{/* legend */}
<div
className="tm-sc"
style={{ display: 'flex', flexWrap: 'wrap', gap: 12, marginBottom: 6, fontSize: 9 }}
>
<Legend glyph="✓" c="var(--tm-up)" label="ok" />
<Legend glyph="⚠" c={C_AMBER} label="throttle" />
<Legend glyph="❌" c="var(--tm-dn)" label="risk" />
</div>
<div className="tm-hair" style={{ marginBottom: 0 }} />
{!cycles.length ? (
<div className="tm-sc" style={{ padding: '16px 0' }}>No execution events yet.</div>
) : (
<div
style={{
height,
overflowY: 'auto',
fontSize: 10,
lineHeight: 1.5,
fontVariantNumeric: 'tabular-nums',
}}
>
{cycles.map((c) => (
<Cycle key={`${c.cycle_number}-${c.timestamp}`} record={c} />
))}
</div>
)}
</div>
)
}
function Legend({ glyph, c, label }: { glyph: string; c: string; label: string }) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
<span style={{ color: c, fontSize: 10 }}>{glyph}</span>
<span>{label}</span>
</span>
)
}
interface CycleProps {
record: DecisionRecord
}
function Cycle({ record }: CycleProps) {
const time = fmtTime(record.timestamp)
const actions = record.decisions ?? []
const logs = record.execution_log ?? []
const count = actions.length
return (
<div style={{ borderBottom: '1px solid var(--tm-hair)', padding: '5px 0' }}>
{/* cycle header bar */}
<div
className="tm-sc"
style={{
display: 'flex',
alignItems: 'center',
gap: 7,
fontSize: 9,
padding: '2px 6px',
marginBottom: actions.length || logs.length || record.error_message ? 4 : 0,
background: 'rgba(26,24,19,0.045)',
borderLeft: `2px solid ${record.success ? 'var(--tm-hair)' : 'var(--tm-dn)'}`,
color: 'var(--tm-ink-2)',
}}
>
<span style={{ color: 'var(--tm-ink)', fontWeight: 700 }}>CYCLE {record.cycle_number}</span>
<span style={{ color: 'var(--tm-muted)' }}>·</span>
<span style={{ color: 'var(--tm-muted)' }}>{time}</span>
<span style={{ marginLeft: 'auto', color: 'var(--tm-muted)' }}>
{count === 0 ? 'no action' : `${count} action${count > 1 ? 's' : ''}`}
</span>
{!record.success ? (
<span style={{ color: 'var(--tm-dn)', fontWeight: 700 }}>FAULT</span>
) : null}
</div>
{/* AI action lines */}
{actions.map((a, i) => {
const side = actionSide(a.action)
const aTime = fmtTime(a.timestamp)
return (
<div
key={`act-${i}`}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '1px 6px',
color: 'var(--tm-ink-2)',
}}
>
<span style={{ color: 'var(--tm-muted)', flex: '0 0 auto', minWidth: 52 }}>
{aTime !== '--:--:--' ? aTime : time}
</span>
<ActionBadge action={a.action} side={side} />
<span style={{ color: 'var(--tm-ink)', fontWeight: 600, flex: '0 0 auto', minWidth: 48 }}>
{baseSymbol(a.symbol)}
</span>
{a.confidence != null ? (
<span style={{ color: 'var(--tm-muted)', flex: '0 0 auto' }}>
conf<span style={{ color: 'var(--tm-ink-2)' }}>{Math.round(a.confidence)}</span>
</span>
) : null}
</div>
)
})}
{/* execution-log result sub-lines */}
{logs.map((line, i) => (
<SubLine key={`log-${i}`} tone={logTone(line)} text={cleanLog(line)} />
))}
{/* fault message */}
{record.error_message ? (
<SubLine tone="risk" text={cleanLog(record.error_message)} />
) : null}
</div>
)
}
function ActionBadge({ action, side }: { action: string; side: Side }) {
const c = sideColor(side)
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
flex: '0 0 auto',
padding: '0 5px',
height: 14,
fontSize: 9,
letterSpacing: '0.04em',
color: c,
border: `1px solid ${c}`,
background:
side === 'long'
? 'rgba(46,139,87,0.08)'
: side === 'short'
? 'rgba(214,67,58,0.08)'
: 'transparent',
borderRadius: 2,
whiteSpace: 'nowrap',
}}
>
{action.toLowerCase()}
</span>
)
}
function SubLine({ tone, text }: { tone: LogTone; text: string }) {
const color = TONE_COLOR[tone]
const glyph = TONE_GLYPH[tone]
return (
<div
style={{
display: 'flex',
gap: 6,
padding: '0 6px 0 10px',
marginLeft: 6,
borderLeft: '1px solid var(--tm-hair)',
color,
}}
>
<span style={{ flex: '0 0 auto', width: 10, textAlign: 'center' }}>{glyph}</span>
<span style={{ wordBreak: 'break-word' }}>{text}</span>
</div>
)
}
export default ExecutionLog

View File

@@ -0,0 +1,150 @@
import { useMemo } from 'react'
import type { FlowMarketItem } from '../../lib/api/data'
interface FlowMarketsProps {
items?: FlowMarketItem[]
window?: string
}
function baseLabel(raw: string): string {
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/[-_]/g, '').replace(/(USDT|USDC|USD)$/, '')
}
function num(s: string): number {
const n = parseFloat(s)
return Number.isFinite(n) ? n : 0
}
function compact(n: number): string {
const a = Math.abs(n)
const sign = n < 0 ? '-' : '+'
if (a >= 1e9) return `${sign}$${(a / 1e9).toFixed(2)}B`
if (a >= 1e6) return `${sign}$${(a / 1e6).toFixed(2)}M`
if (a >= 1e3) return `${sign}$${(a / 1e3).toFixed(1)}K`
return `${sign}$${a.toFixed(0)}`
}
// Shared 5-column grid so the header, every row, and the legend line up exactly.
// symbol | net inflow | buy/sell split bar | trades | last price
const GRID = '64px 96px minmax(120px, 1fr) 80px 96px'
/**
* FlowMarkets renders the Vergex net-flow ranking (real data from
* GET /api/vergex/flow-markets via the trader's claw402 wallet). Each row shows
* a market's net inflow over the window, a buy/sell split bar, trade count, and
* latest price. Sorted by net inflow descending (the upstream ordering).
*/
export function FlowMarkets({ items, window = '1h' }: FlowMarketsProps) {
const win = window.toUpperCase()
const rows = useMemo(() => {
if (!items || items.length === 0) return []
const max = items.reduce((m, it) => Math.max(m, Math.abs(num(it.netFlow))), 1)
return items.slice(0, 10).map((it) => {
const buy = num(it.buyNotional)
const sell = num(it.sellNotional)
const tot = buy + sell || 1
const net = num(it.netFlow)
return {
key: it.key || it.symbol,
label: baseLabel(it.symbol),
net,
netStr: compact(net),
buyPct: (buy / tot) * 100,
widthPct: (Math.abs(net) / max) * 100,
trades: it.trades,
price: num(it.latestPrice),
}
})
}, [items])
if (rows.length === 0) {
return <div className="tm-sc" style={{ padding: '12px 0' }}>No net-flow data (claw402 payment required).</div>
}
return (
<div className="tm-mono" style={{ fontSize: 11 }}>
{/* column header — every number below is labeled by this row */}
<div
className="tm-sc"
style={{
display: 'grid',
gridTemplateColumns: GRID,
alignItems: 'end',
gap: 12,
paddingBottom: 4,
borderBottom: '1px solid var(--tm-hair)',
fontSize: 9,
}}
>
<span>SYMBOL</span>
<span style={{ textAlign: 'right' }}>{win} NET</span>
<span>BUY/SELL</span>
<span style={{ textAlign: 'right' }}>TRADES</span>
<span style={{ textAlign: 'right' }}>PRICE</span>
</div>
{/* rows */}
{rows.map((r) => (
<div
key={r.key}
style={{
display: 'grid',
gridTemplateColumns: GRID,
alignItems: 'center',
gap: 12,
height: 26,
borderBottom: '1px solid var(--tm-hair)',
}}
>
{/* symbol */}
<span style={{ fontWeight: 600, color: 'var(--tm-ink)' }}>{r.label}</span>
{/* net inflow figure (green = net buying / red = net selling) */}
<span className={r.net >= 0 ? 'tm-up' : 'tm-dn'} style={{ textAlign: 'right', fontWeight: 600 }}>
{r.netStr}
</span>
{/* buy/sell split bar — width encodes net-inflow magnitude (vs. the top
market), the green/red split inside encodes buy vs. sell share.
Green grows from the LEFT (buy), red fills the REST (sell). */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div
title={`buy ${r.buyPct.toFixed(0)}% / sell ${(100 - r.buyPct).toFixed(0)}%`}
style={{
position: 'relative',
height: 8,
flex: 1,
minWidth: 40,
background: 'var(--tm-hair)',
}}
>
<div style={{ position: 'absolute', inset: 0, width: `${Math.max(4, r.widthPct)}%`, display: 'flex' }}>
<div style={{ width: `${r.buyPct}%`, background: 'var(--tm-up)' }} />
<div style={{ width: `${100 - r.buyPct}%`, background: 'var(--tm-dn)' }} />
</div>
</div>
<span className="tm-sc" style={{ fontSize: 9, minWidth: 30, textAlign: 'right' }}>
{r.buyPct.toFixed(0)}%
</span>
</div>
{/* trade count */}
<span style={{ textAlign: 'right', color: 'var(--tm-ink-2)' }}>
{r.trades.toLocaleString('en-US')}
</span>
{/* last price */}
<span style={{ textAlign: 'right', color: 'var(--tm-ink-2)' }}>
${r.price.toLocaleString('en-US', { maximumFractionDigits: 4 })}
</span>
</div>
))}
{/* legend — explains every column */}
<div className="tm-sc" style={{ marginTop: 8, fontSize: 9, lineHeight: 1.6 }}>
net inflow = {win} net buying · <span className="tm-up">green</span>/<span className="tm-dn">red</span> = buy/sell split
{' · '}trades = count · last price = last traded price
</div>
</div>
)
}
export default FlowMarkets

View File

@@ -0,0 +1,166 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import useSWR from 'swr'
import { api } from '../../lib/api'
import type { Kline } from '../../lib/api/data'
import { Candles } from './Candles'
/**
* KlineChart shows a live candlestick chart. History is seeded from the backend
* kline endpoint, then the latest bar streams in real time from Hyperliquid's
* public `candle` WebSocket (the forming candle ticks live and rolls over each
* interval). Resolves crypto majors to the main dex and synthetic markets to
* the `xyz:` builder dex, matching the order book.
*
* Real OHLC only — no synthetic data.
*/
const HL_INFO = 'https://api.hyperliquid.xyz/info'
const HL_WS = 'wss://api.hyperliquid.xyz/ws'
const INTERVAL = '1m'
const MAX_BARS = 90
function baseSymbol(raw: string): string {
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/(USDT|USDC|USD)$/, '')
}
interface KlineChartProps {
symbol: string
/** target chart height in px (ignored when fill) */
height?: number
/** stretch the chart to fill the parent's remaining height */
fill?: boolean
}
export function KlineChart({ symbol, height = 360, fill = false }: KlineChartProps) {
const base = baseSymbol(symbol || '')
// history seed (resynced occasionally; the WS carries the live bar)
const { data: seed, isLoading } = useSWR(
base ? ['kline', base, INTERVAL] : null,
() => api.getKlines(base, INTERVAL, 'hyperliquid', MAX_BARS, true),
{ refreshInterval: 60000, revalidateOnFocus: false, shouldRetryOnError: false, keepPreviousData: true },
)
// resolve the Hyperliquid coin id (xyz: dex membership)
const [xyzSet, setXyzSet] = useState<Set<string>>(new Set())
useEffect(() => {
let alive = true
fetch(HL_INFO, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'allMids', dex: 'xyz' }) })
.then((r) => r.json())
.then((mids: Record<string, string>) => {
if (!alive) return
const set = new Set<string>()
for (const k of Object.keys(mids || {})) set.add(k.replace(/^xyz:/, '').toUpperCase())
setXyzSet(set)
})
.catch(() => {})
return () => {
alive = false
}
}, [])
const coin = useMemo(() => (base ? (xyzSet.has(base) ? `xyz:${base}` : base) : ''), [base, xyzSet])
// live bar from the candle WS
const [liveBar, setLiveBar] = useState<Kline | null>(null)
const [wsLive, setWsLive] = useState(false)
const pending = useRef<Kline | null>(null)
useEffect(() => {
if (!coin) return
setLiveBar(null)
let ws: WebSocket | null = null
let raf: number | null = null
let retry: ReturnType<typeof setTimeout> | null = null
let closed = false
const connect = () => {
ws = new WebSocket(HL_WS)
ws.onopen = () => ws?.send(JSON.stringify({ method: 'subscribe', subscription: { type: 'candle', coin, interval: INTERVAL } }))
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data)
if (msg.channel !== 'candle' || !msg.data) return
const d = msg.data
pending.current = { openTime: d.t, closeTime: d.T, open: +d.o, high: +d.h, low: +d.l, close: +d.c, volume: +d.v }
setWsLive(true)
} catch {
/* ignore */
}
}
ws.onclose = () => {
if (closed) return
setWsLive(false)
retry = setTimeout(connect, 1500)
}
ws.onerror = () => ws?.close()
}
connect()
const loop = () => {
if (pending.current) {
setLiveBar(pending.current)
pending.current = null
}
raf = requestAnimationFrame(loop)
}
raf = requestAnimationFrame(loop)
return () => {
closed = true
if (raf) cancelAnimationFrame(raf)
if (retry) clearTimeout(retry)
try {
ws?.send(JSON.stringify({ method: 'unsubscribe', subscription: { type: 'candle', coin, interval: INTERVAL } }))
} catch {
/* socket gone */
}
ws?.close()
}
}, [coin])
// merge the live bar into the seeded history
const candles = useMemo(() => {
const hist = seed ?? []
if (!liveBar) return hist
const arr = [...hist]
const last = arr[arr.length - 1]
if (last && liveBar.openTime === last.openTime) arr[arr.length - 1] = liveBar
else if (!last || liveBar.openTime > last.openTime) arr.push(liveBar)
return arr.slice(-MAX_BARS)
}, [seed, liveBar])
const last = candles.length ? candles[candles.length - 1].close : 0
const first = candles.length ? candles[0].open : 0
const chg = first ? ((last - first) / first) * 100 : 0
const live = wsLive && candles.length > 0
return (
<div style={{ fontFamily: 'var(--tm-mono)', ...(fill ? { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 } : {}) }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>{base || 'MARKET'}</span>
<span className="tm-sc">{INTERVAL} · Live candles</span>
<span className="tm-sc" style={{ marginLeft: 'auto', color: live ? 'var(--tm-up)' : 'var(--tm-muted)' }}>
{live ? '● live' : isLoading || candles.length ? '○ sync' : '○ —'}
</span>
</div>
{last > 0 && (
<div className="tm-mono" style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 4, fontSize: 12 }}>
<span style={{ fontWeight: 600 }}>${last.toLocaleString('en-US', { maximumFractionDigits: 4 })}</span>
<span className={chg >= 0 ? 'tm-up' : 'tm-dn'} style={{ fontSize: 11 }}>{chg >= 0 ? '+' : ''}{chg.toFixed(2)}%</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>{candles.length} bars · {INTERVAL}</span>
</div>
)}
{candles.length > 0 ? (
fill ? (
<div style={{ flex: 1, minHeight: 0 }}>
<Candles data={candles} width={380} height={height} fill />
</div>
) : (
<Candles data={candles} width={380} height={height} />
)
) : (
<div className="tm-sc" style={{ padding: '20px 0' }}>Loading market</div>
)}
</div>
)
}
export default KlineChart

View File

@@ -0,0 +1,236 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import useSWR from 'swr'
import { api } from '../../lib/api'
import type { VergexHeatmapBin } from '../../lib/api/data'
/**
* LiquidationMap renders the vergex (claw402) cost / liquidation heatmap as a
* vertical price ladder — position-cost concentration plus liquidation fuel by
* price level. Long metrics diverge right, short metrics diverge left from the
* mark price. Cream-themed adaptation of a Bloomberg-style liquidation map.
*
* Real paid data only (hip3_perp synthetic markets). Polled at 5 min to spare
* the claw402 wallet.
*/
const C_LONG_COST = 'var(--tm-up)' // forest green
const C_SHORT_COST = 'var(--tm-dn)' // crimson
const C_LONG_LIQ = '#c8860b' // amber — long-liquidation fuel (price falls)
const C_SHORT_LIQ = '#2c7a9e' // teal — short-liquidation fuel (price rises)
function fmtUsd(n: number): string {
const a = Math.abs(n)
if (a >= 1e9) return `$${(n / 1e9).toFixed(2)}B`
if (a >= 1e6) return `$${(n / 1e6).toFixed(2)}M`
if (a >= 1e3) return `$${(n / 1e3).toFixed(1)}K`
return `$${n.toFixed(0)}`
}
function fmtPx(n: number): string {
if (n >= 1000) return n.toLocaleString('en-US', { maximumFractionDigits: 0 })
if (n >= 1) return n.toLocaleString('en-US', { maximumFractionDigits: 2 })
return n.toLocaleString('en-US', { maximumFractionDigits: 4 })
}
interface Row extends VergexHeatmapBin {
px: number
longCost: number
shortCost: number
longLiq: number
shortLiq: number
}
interface LiquidationMapProps {
symbol: string
marketType?: string
/** fixed height of the scrollable ladder (px); auto-centres on the mark */
height?: number
}
export function LiquidationMap({ symbol, marketType = 'hip3_perp', height = 460 }: LiquidationMapProps) {
// Synthetic markets live under marketType "hip3_perp"; crypto majors under
// "perp". We try the caller's guess first and fall back to the other so the
// heatmap resolves for ANY symbol that has one.
const fetcher = (mt: string) =>
api.getVergexCostLiquidationHeatmap({ marketType: mt, symbol, chain: 'mainnet', liqBand: '15' })
const opts = { refreshInterval: 300000, revalidateOnFocus: false, keepPreviousData: true }
const primary = useSWR(symbol ? ['heatmap', marketType, symbol] : null, () => fetcher(marketType), opts)
const primaryHasBins = !!primary.data?.data?.bins?.length
const altMt = marketType === 'perp' ? 'hip3_perp' : 'perp'
const needAlt = !primaryHasBins && !primary.isLoading && primary.data !== undefined
const alt = useSWR(needAlt && symbol ? ['heatmap', altMt, symbol] : null, () => fetcher(altMt), opts)
const data = primaryHasBins ? primary.data : alt.data
const isLoading = primary.isLoading || (needAlt && alt.isLoading)
const error = primaryHasBins ? undefined : alt.error || primary.error
const [hover, setHover] = useState<number | null>(null)
const scrollRef = useRef<HTMLDivElement>(null)
const markRef = useRef<HTMLDivElement>(null)
const view = useMemo(() => {
const d = data?.data
// keepPreviousData can leave a stale heatmap on screen when the selected
// symbol has no hip3 market (e.g. a crypto major) — detect the mismatch and
// treat it as no-data so the panel honestly reflects the requested symbol.
const requested = (symbol || '').toUpperCase().replace(/^XYZ:/, '')
const loaded = (d?.market?.symbol || '').toUpperCase().replace(/^XYZ:/, '')
const stale = !!loaded && loaded !== requested
const raw = stale ? [] : d?.bins ?? []
const rows: Row[] = raw
.map((b) => ({
px: b.px ?? ((b.bucketStartPrice ?? 0) + (b.bucketEndPrice ?? 0)) / 2,
longCost: b.longCost ?? 0,
shortCost: b.shortCost ?? 0,
longLiq: b.longLiq ?? 0,
shortLiq: b.shortLiq ?? 0,
...b,
}))
.filter((r) => r.px > 0 && (r.longCost || r.shortCost || r.longLiq || r.shortLiq))
.sort((a, b) => b.px - a.px)
const maxSide = rows.reduce(
(m, r) => Math.max(m, r.longCost + r.longLiq, r.shortCost + r.shortLiq),
1,
)
const totals = rows.reduce(
(t, r) => ({
lc: t.lc + r.longCost,
sc: t.sc + r.shortCost,
ll: t.ll + r.longLiq,
sl: t.sl + r.shortLiq,
}),
{ lc: 0, sc: 0, ll: 0, sl: 0 },
)
return { rows, maxSide, mark: stale ? 0 : d?.markPrice ?? 0, costAddrs: stale ? 0 : d?.costAddrs ?? 0, liqAddrs: stale ? 0 : d?.liqAddrs ?? 0, totals, dispSymbol: stale ? symbol : d?.market?.symbol || symbol }
}, [data, symbol])
const markRowIdx = useMemo(() => {
if (!view.mark || !view.rows.length) return -1
let best = 0
let bd = Infinity
view.rows.forEach((r, i) => {
const dd = Math.abs(r.px - view.mark)
if (dd < bd) {
bd = dd
best = i
}
})
return best
}, [view])
// centre the scroll ladder on the mark (current) price once data arrives.
// Uses bounding-rect math (not offsetTop, which depends on the offsetParent)
// and re-applies on the next frame so it lands after layout settles.
useEffect(() => {
const sc = scrollRef.current
const mk = markRef.current
if (!sc || !mk) return
const apply = () => {
const rel = mk.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop
sc.scrollTop = rel - sc.clientHeight / 2 + mk.offsetHeight / 2
}
apply()
const id = requestAnimationFrame(apply)
return () => cancelAnimationFrame(id)
}, [markRowIdx, view.rows.length, height, view.dispSymbol])
const rowH = view.rows.length > 44 ? 8 : view.rows.length > 28 ? 11 : 15
const hv = hover != null ? view.rows[hover] : null
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 3 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Cost / Liq map</span>
<span className="tm-sc">{view.dispSymbol}</span>
<span className="tm-sc" style={{ marginLeft: 'auto', color: view.rows.length ? 'var(--tm-up)' : 'var(--tm-muted)' }}>
{view.rows.length ? '● live' : isLoading ? '○ sync' : '○ —'}
</span>
</div>
{/* legend */}
<div className="tm-sc" style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 4, fontSize: 9 }}>
<Swatch c={C_LONG_COST} label="Long cost" />
<Swatch c={C_SHORT_COST} label="Short cost" />
<Swatch c={C_LONG_LIQ} label="Long liq" />
<Swatch c={C_SHORT_LIQ} label="Short liq" />
</div>
{/* hover readout / mark line */}
<div className="tm-mono" style={{ fontSize: 10, color: 'var(--tm-ink-2)', minHeight: 14, marginBottom: 2 }}>
{hv ? (
<span>
<b>{fmtPx(hv.px)}</b> · Cost line <span style={{ color: C_LONG_COST }}>{fmtUsd(hv.longCost)}</span>/<span style={{ color: C_SHORT_COST }}>{fmtUsd(hv.shortCost)}</span>
{' · '}liq <span style={{ color: C_LONG_LIQ }}>{fmtUsd(hv.longLiq)}</span>/<span style={{ color: C_SHORT_LIQ }}>{fmtUsd(hv.shortLiq)}</span>
</span>
) : (
<span className="tm-sc">mark <b style={{ color: 'var(--tm-red)' }}>{view.mark ? fmtPx(view.mark) : '—'}</b> · {view.costAddrs.toLocaleString()} positions / {view.liqAddrs.toLocaleString()} liq levels</span>
)}
</div>
{error && !view.rows.length ? (
<div className="tm-sc" style={{ padding: '16px 0' }}>No cost/liq heatmap for {view.dispSymbol} (crypto / main-dex markets have none).</div>
) : !view.rows.length ? (
<div className="tm-sc" style={{ padding: '16px 0' }}>Loading cost/liquidation map</div>
) : (
<div>
<div ref={scrollRef} style={{ maxHeight: height, overflowY: 'auto' }}>
{view.rows.map((r, i) => {
const isMark = i === markRowIdx
const lcW = (r.longCost / view.maxSide) * 100
const llW = (r.longLiq / view.maxSide) * 100
const scW = (r.shortCost / view.maxSide) * 100
const slW = (r.shortLiq / view.maxSide) * 100
const showLabel = i % 4 === 0 || isMark
return (
<div
key={i}
ref={isMark ? markRef : undefined}
onMouseEnter={() => setHover(i)}
onMouseLeave={() => setHover(null)}
style={{
display: 'grid',
gridTemplateColumns: '52px 1fr 1fr',
alignItems: 'center',
height: rowH,
background: hover === i ? 'rgba(26,24,19,0.05)' : 'transparent',
borderTop: isMark ? '1px solid var(--tm-red)' : 'none',
}}
>
<span style={{ fontSize: 9, textAlign: 'right', paddingRight: 6, color: isMark ? 'var(--tm-red)' : 'var(--tm-muted)', fontWeight: isMark ? 700 : 400 }}>
{showLabel ? fmtPx(r.px) : ''}
</span>
{/* short side — bars anchored at center, extend left (cost nearest center) */}
<div style={{ position: 'relative', height: '100%', display: 'flex', justifyContent: 'flex-end' }}>
<div style={{ width: `${slW}%`, background: C_SHORT_LIQ, opacity: 0.85 }} />
<div style={{ width: `${scW}%`, background: C_SHORT_COST }} />
</div>
{/* long side — bars from center, extend right (cost nearest center) */}
<div style={{ position: 'relative', height: '100%', display: 'flex', justifyContent: 'flex-start' }}>
<div style={{ width: `${lcW}%`, background: C_LONG_COST }} />
<div style={{ width: `${llW}%`, background: C_LONG_LIQ, opacity: 0.85 }} />
</div>
</div>
)
})}
</div>
{/* totals footer */}
<div className="tm-sc" style={{ display: 'flex', gap: 10, marginTop: 4, fontSize: 9, flexWrap: 'wrap' }}>
<span>Cost line <span style={{ color: C_LONG_COST }}>{fmtUsd(view.totals.lc)}</span>/<span style={{ color: C_SHORT_COST }}>{fmtUsd(view.totals.sc)}</span></span>
<span>liq <span style={{ color: C_LONG_LIQ }}>{fmtUsd(view.totals.ll)}</span>/<span style={{ color: C_SHORT_LIQ }}>{fmtUsd(view.totals.sl)}</span></span>
</div>
</div>
)}
</div>
)
}
function Swatch({ c, label }: { c: string; label: string }) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
<span style={{ width: 8, height: 8, background: c, display: 'inline-block' }} />
{label}
</span>
)
}
export default LiquidationMap

View File

@@ -0,0 +1,213 @@
import { useMemo } from 'react'
/**
* OrchestrationTopology renders the decision funnel as a chain of tightly-packed
* dot matrices — one wide grid per layer (flow → signal → decision → execute →
* hold), columns sitting close together. Every grid is always full: real symbols
* are SOLID (green long / red short) and SCATTERED across the grid; empty cells
* are HOLLOW placeholders. Matching symbols connect forward; the engine fans
* beams to the candidates that progress.
*/
export interface FunnelItem {
symbol: string
dir: 'long' | 'short'
}
export interface FunnelLayer {
key: string
title: string
zh: string
items: FunnelItem[]
}
interface OrchestrationTopologyProps {
layers: FunnelLayer[]
className?: string
}
const VB_W = 800
const ENGINE_X = 40
const HEADER_Y = 30
const COLS = 8
const LONG_ROWS = 5
const SHORT_ROWS = 5
const CAP_LONG = COLS * LONG_ROWS // 40
const CAP_SHORT = COLS * SHORT_ROWS // 40
const DX = 11
const DY = 11
const ZONE_GAP = 8
const LAYER_START = 100
const LAYER_STEP = 128
const LONG = 'var(--tm-up)'
const SHORT = 'var(--tm-dn)'
function baseSymbol(raw: string): string {
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/[-_]/g, '').replace(/(USDT|USDC|USD)$/, '')
}
// evenly spread `count` solid items across `capacity` grid cells
function scatter(count: number, capacity: number): Map<number, number> {
const m = new Map<number, number>()
if (count <= 0) return m
for (let i = 0; i < count; i++) {
const cell = Math.min(capacity - 1, Math.floor(((i + 0.5) * capacity) / count))
m.set(cell, i)
}
return m
}
interface Cell {
base?: string
x: number
y: number
dir: 'long' | 'short'
solid: boolean
}
function splitDedup(items: FunnelItem[]) {
const seen = new Set<string>()
const longs: string[] = []
const shorts: string[] = []
for (const it of items) {
const b = baseSymbol(it.symbol)
if (!b || seen.has(b)) continue
seen.add(b)
;(it.dir === 'short' ? shorts : longs).push(b)
}
return { longs: longs.slice(0, CAP_LONG), shorts: shorts.slice(0, CAP_SHORT) }
}
export function OrchestrationTopology({ layers, className }: OrchestrationTopologyProps) {
const { prepared, cellsByLayer, realByLayer, height, cy, colX } = useMemo(() => {
const prep = layers.map((l) => ({ ...l, ...splitDedup(l.items) }))
const xs = prep.map((_, i) => LAYER_START + i * LAYER_STEP)
const gridH = (LONG_ROWS + SHORT_ROWS) * DY + ZONE_GAP
const h = HEADER_Y + gridH + 14
const centerY = HEADER_Y + gridH / 2
const longTop = centerY - gridH / 2
const shortTop = longTop + LONG_ROWS * DY + ZONE_GAP
const cells: Cell[][] = []
const realMaps: Map<string, { x: number; y: number; dir: 'long' | 'short' }>[] = []
prep.forEach((d, li) => {
const layerCells: Cell[] = []
const real = new Map<string, { x: number; y: number; dir: 'long' | 'short' }>()
const longScatter = scatter(d.longs.length, CAP_LONG)
const shortScatter = scatter(d.shorts.length, CAP_SHORT)
for (let idx = 0; idx < CAP_LONG; idx++) {
const c = idx % COLS
const r = Math.floor(idx / COLS)
const x = xs[li] + c * DX
const y = longTop + r * DY
const itemIdx = longScatter.get(idx)
const base = itemIdx !== undefined ? d.longs[itemIdx] : undefined
layerCells.push({ base, x, y, dir: 'long', solid: !!base })
if (base) real.set(base, { x, y, dir: 'long' })
}
for (let idx = 0; idx < CAP_SHORT; idx++) {
const c = idx % COLS
const r = Math.floor(idx / COLS)
const x = xs[li] + c * DX
const y = shortTop + r * DY
const itemIdx = shortScatter.get(idx)
const base = itemIdx !== undefined ? d.shorts[itemIdx] : undefined
layerCells.push({ base, x, y, dir: 'short', solid: !!base })
if (base) real.set(base, { x, y, dir: 'short' })
}
cells.push(layerCells)
realMaps.push(real)
})
return { prepared: prep, cellsByLayer: cells, realByLayer: realMaps, height: h, cy: centerY, colX: xs }
}, [layers])
const edges = useMemo(() => {
const out: { x1: number; y1: number; x2: number; y2: number; dir: string; key: string }[] = []
for (let l = 0; l < realByLayer.length - 1; l++) {
const right = realByLayer[l + 1]
if (right.size === 0) continue
realByLayer[l].forEach((a, base) => {
const b = right.get(base)
if (b) out.push({ x1: a.x, y1: a.y, x2: b.x, y2: b.y, dir: a.dir, key: `${l}-${base}` })
})
}
return out
}, [realByLayer])
// engine fans to real nodes in the first non-empty layer — balanced across
// long (top) and short (bottom) so both halves get dispatch lines/beams
const engineTargets = useMemo(() => {
const idx = realByLayer.findIndex((m) => m.size > 0)
if (idx < 0) return [] as { x: number; y: number; dir: 'long' | 'short' }[]
const all = [...realByLayer[idx].values()]
const longs = all.filter((n) => n.dir === 'long').slice(0, 24)
const shorts = all.filter((n) => n.dir === 'short').slice(0, 24)
return [...longs, ...shorts]
}, [realByLayer])
return (
<svg width="100%" viewBox={`0 0 ${VB_W} ${height}`} role="img"
aria-label="Decision funnel matrix: flow, signal, decision, execute, hold"
className={className} style={{ display: 'block' }}>
{prepared.map((d, li) => (
<g key={d.key} fontFamily="var(--tm-mono)">
<text x={colX[li] - 2} y={12} fontSize={8} fill="var(--tm-muted)" style={{ letterSpacing: '0.08em' }}>{d.title}</text>
<text x={colX[li] - 2} y={23} fontSize={8}>
<tspan fill="var(--tm-up)">L{d.longs.length}</tspan>
<tspan fill="var(--tm-muted)"> </tspan>
<tspan fill="var(--tm-dn)">S{d.shorts.length}</tspan>
</text>
</g>
))}
<g strokeWidth={0.6} strokeDasharray="2 3" opacity={0.4}>
{engineTargets.map((n, i) => (
<line key={i} x1={ENGINE_X} y1={cy} x2={n.x} y2={n.y} stroke={n.dir === 'short' ? SHORT : LONG} />
))}
</g>
<g strokeWidth={0.8} strokeDasharray="2 3" opacity={0.5}>
{edges.map((e) => (
<line key={e.key} x1={e.x1} y1={e.y1} x2={e.x2} y2={e.y2} stroke={e.dir === 'short' ? SHORT : LONG} />
))}
</g>
{engineTargets.map((n, i) => (
<circle key={`b0-${i}`} r={1.8} fill={n.dir === 'short' ? SHORT : LONG}>
<animate attributeName="cx" values={`${ENGINE_X};${n.x}`} dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
<animate attributeName="cy" values={`${cy};${n.y}`} dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
<animate attributeName="opacity" values="0.9;0.9;0" dur={`${0.5 + (i % 5) * 0.08}s`} begin={`${(i % 8) * 0.07}s`} repeatCount="indefinite" />
</circle>
))}
{edges.map((e, i) => (
<circle key={`be-${e.key}`} r={2.1} fill={e.dir === 'short' ? SHORT : LONG}>
<animate attributeName="cx" values={`${e.x1};${e.x2}`} dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
<animate attributeName="cy" values={`${e.y1};${e.y2}`} dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
<animate attributeName="opacity" values="1;1;0" dur={`${0.45 + (i % 4) * 0.08}s`} begin={`${(i % 6) * 0.06}s`} repeatCount="indefinite" />
</circle>
))}
{cellsByLayer.map((layer, li) =>
layer.map((cell, ci) => {
const color = cell.dir === 'short' ? SHORT : LONG
if (cell.solid) {
return <circle key={`${li}-${ci}`} cx={cell.x} cy={cell.y} r={2.7} fill={color} stroke={color} strokeWidth={0.5} />
}
return <circle key={`${li}-${ci}`} cx={cell.x} cy={cell.y} r={1.8} fill="none" stroke={color} strokeWidth={0.6} opacity={0.2} />
})
)}
<circle cx={ENGINE_X} cy={cy} r={18} fill="none" stroke="var(--tm-red)" strokeWidth={1.5}>
<animate attributeName="r" values="18;34" dur="2.2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.6;0" dur="2.2s" repeatCount="indefinite" />
</circle>
<circle cx={ENGINE_X} cy={cy} r={18} fill="var(--tm-red)" />
<text x={ENGINE_X} y={cy + 3} textAnchor="middle" fontFamily="var(--tm-px)" fontSize={7} fill="var(--tm-paper)">NOFX</text>
</svg>
)
}
export default OrchestrationTopology

View File

@@ -0,0 +1,311 @@
import { useEffect, useMemo, useRef, useState } from 'react'
/**
* OrderBook renders a live L2 depth ladder for a single instrument, streamed
* directly from Hyperliquid's public WebSocket (`l2Book`). The app trades a
* Hyperliquid builder-deployed perp DEX named "xyz" for synthetic / equity
* markets (xyz:SP500, xyz:SKHX, …) and the main dex for crypto majors
* (BTC, ETH, …). We resolve which one a symbol belongs to from the xyz dex's
* `allMids` coin set, then subscribe to the matching `l2Book` feed.
*
* Real data only — no synthetic depth.
*/
const HL_INFO = 'https://api.hyperliquid.xyz/info'
const HL_WS = 'wss://api.hyperliquid.xyz/ws'
const DEPTH = 11 // levels per side
interface Level {
px: number
sz: number
}
interface BookState {
coin: string
bids: Level[]
asks: Level[]
}
function baseSymbol(raw: string): string {
return raw
.toUpperCase()
.replace(/^XYZ:/, '')
.replace(/(USDT|USDC|USD)$/, '')
}
// Resolve a base symbol to the Hyperliquid coin id. Members of the xyz dex get
// the "xyz:" prefix; everything else is treated as a main-dex coin.
function resolveCoin(base: string, xyzSet: Set<string>): string {
if (!base) return ''
return xyzSet.has(base) ? `xyz:${base}` : base
}
function fmtPx(px: number): string {
if (px >= 1000) return px.toLocaleString('en-US', { maximumFractionDigits: 1 })
if (px >= 1) return px.toLocaleString('en-US', { maximumFractionDigits: 3 })
return px.toLocaleString('en-US', { maximumFractionDigits: 5 })
}
function fmtSz(sz: number): string {
if (sz >= 1000) return `${(sz / 1000).toFixed(1)}k`
if (sz >= 1) return sz.toFixed(2)
return sz.toFixed(3)
}
interface OrderBookProps {
/** raw business symbol (e.g. position symbol or candidate coin) */
symbol: string
/** optional entry price to mark the user's position level on the ladder */
markPrice?: number
}
export function OrderBook({ symbol, markPrice }: OrderBookProps) {
const base = useMemo(() => baseSymbol(symbol || ''), [symbol])
const [xyzSet, setXyzSet] = useState<Set<string>>(new Set())
const [book, setBook] = useState<BookState | null>(null)
const [status, setStatus] = useState<'connecting' | 'live' | 'down'>('connecting')
// one-time: fetch the xyz dex coin universe so we can resolve symbols
useEffect(() => {
let alive = true
fetch(HL_INFO, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'allMids', dex: 'xyz' }),
})
.then((r) => r.json())
.then((mids: Record<string, string>) => {
if (!alive) return
const set = new Set<string>()
for (const k of Object.keys(mids || {})) set.add(k.replace(/^xyz:/, '').toUpperCase())
setXyzSet(set)
})
.catch(() => {
/* CORS / offline — fall back to main-dex resolution (empty set) */
})
return () => {
alive = false
}
}, [])
const coin = useMemo(() => resolveCoin(base, xyzSet), [base, xyzSet])
// live L2 stream
const pending = useRef<BookState | null>(null)
useEffect(() => {
if (!coin) return
let ws: WebSocket | null = null
let raf: number | null = null
let retry: ReturnType<typeof setTimeout> | null = null
let closed = false
const connect = () => {
setStatus('connecting')
ws = new WebSocket(HL_WS)
ws.onopen = () => {
ws?.send(JSON.stringify({ method: 'subscribe', subscription: { type: 'l2Book', coin } }))
}
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data)
if (msg.channel !== 'l2Book' || !msg.data) return
const lv = msg.data.levels
if (!Array.isArray(lv) || lv.length < 2) return
const toLevels = (arr: { px: string; sz: string }[]): Level[] =>
arr.slice(0, DEPTH).map((l) => ({ px: parseFloat(l.px), sz: parseFloat(l.sz) }))
pending.current = { coin: msg.data.coin, bids: toLevels(lv[0]), asks: toLevels(lv[1]) }
setStatus('live')
} catch {
/* ignore malformed frame */
}
}
ws.onclose = () => {
if (closed) return
setStatus('down')
retry = setTimeout(connect, 1500)
}
ws.onerror = () => ws?.close()
}
connect()
// flush on every animation frame (~16ms) so individual rows tick the instant
// the venue pushes a change — millisecond-level, not a batched interval
const loop = () => {
if (pending.current) {
setBook(pending.current)
pending.current = null
}
raf = requestAnimationFrame(loop)
}
raf = requestAnimationFrame(loop)
return () => {
closed = true
if (raf) cancelAnimationFrame(raf)
if (retry) clearTimeout(retry)
try {
ws?.send(JSON.stringify({ method: 'unsubscribe', subscription: { type: 'l2Book', coin } }))
} catch {
/* socket already gone */
}
ws?.close()
}
}, [coin])
const view = useMemo(() => {
if (!book) return null
const asks = book.asks.slice(0, DEPTH)
const bids = book.bids.slice(0, DEPTH)
// cumulative depth for background bars
let ca = 0
const askRows = asks.map((l) => ({ ...l, cum: (ca += l.sz) }))
let cb = 0
const bidRows = bids.map((l) => ({ ...l, cum: (cb += l.sz) }))
const maxCum = Math.max(ca, cb, 1)
const bestAsk = asks[0]?.px ?? 0
const bestBid = bids[0]?.px ?? 0
const mid = bestAsk && bestBid ? (bestAsk + bestBid) / 2 : 0
const spread = bestAsk && bestBid ? bestAsk - bestBid : 0
const spreadBps = mid ? (spread / mid) * 10000 : 0
// buy/sell pressure across the visible book (by notional)
const bidVol = bidRows.reduce((s, l) => s + l.sz * l.px, 0)
const askVol = askRows.reduce((s, l) => s + l.sz * l.px, 0)
const bidPct = bidVol + askVol > 0 ? (bidVol / (bidVol + askVol)) * 100 : 50
// the single visible level nearest the user's entry — marked with ▸
let markLevel: number | undefined
if (markPrice) {
let bd = Infinity
for (const l of [...asks, ...bids]) {
const d = Math.abs(l.px - markPrice)
if (d < bd) {
bd = d
markLevel = l.px
}
}
}
return { askRows: askRows.reverse(), bidRows, maxCum, mid, spread, spreadBps, bidPct, markLevel }
}, [book, markPrice])
const rowH = 16
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Order book</span>
<span className="tm-sc">L2 · {coin || base || '—'}</span>
<span
className="tm-sc"
style={{ marginLeft: 'auto', color: status === 'live' ? 'var(--tm-up)' : 'var(--tm-muted)' }}
>
{status === 'live' ? '● live' : status === 'connecting' ? '○ sync' : '○ down'}
</span>
</div>
{!view ? (
<div className="tm-sc" style={{ padding: '16px 0' }}>Connecting to Hyperliquid</div>
) : (
<div style={{ fontSize: 11 }}>
{/* column header */}
<div className="tm-sc" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4, marginBottom: 2 }}>
<span>price</span>
<span style={{ textAlign: 'right' }}>size</span>
<span style={{ textAlign: 'right' }}>cum $</span>
</div>
{/* asks (red), best ask nearest the mid — keyed by PRICE so each level
keeps its identity and flashes independently when its size changes */}
{view.askRows.map((l) => (
<Row key={`a-${l.px}`} px={l.px} sz={l.sz} cum={l.cum} maxCum={view.maxCum} side="ask" h={rowH} mark={view.markLevel} />
))}
{/* mid / spread */}
<div
className="tm-mono"
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '3px 0',
margin: '2px 0',
borderTop: '1px solid var(--tm-hair)',
borderBottom: '1px solid var(--tm-hair)',
}}
>
<span className="tm-px" style={{ fontSize: 12, color: 'var(--tm-red)' }}>{fmtPx(view.mid)}</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>spread {fmtPx(view.spread)} · {view.spreadBps.toFixed(1)}bps</span>
</div>
{/* bids (green) — keyed by price, same independent-flash behavior */}
{view.bidRows.map((l) => (
<Row key={`b-${l.px}`} px={l.px} sz={l.sz} cum={l.cum} maxCum={view.maxCum} side="bid" h={rowH} mark={view.markLevel} />
))}
{/* buy/sell pressure across the visible book */}
<div style={{ marginTop: 7 }}>
<div style={{ display: 'flex', height: 6 }}>
<div style={{ width: `${view.bidPct}%`, background: 'var(--tm-up)', transition: 'width 0.2s ease-out' }} />
<div style={{ flex: 1, background: 'var(--tm-dn)' }} />
</div>
<div className="tm-sc" style={{ display: 'flex', fontSize: 9, marginTop: 2 }}>
<span className="tm-up">B {view.bidPct.toFixed(1)}%</span>
<span style={{ marginLeft: 'auto' }} className="tm-dn">{(100 - view.bidPct).toFixed(1)}% S</span>
</div>
</div>
</div>
)}
</div>
)
}
interface RowProps {
px: number
sz: number
cum: number
maxCum: number
side: 'ask' | 'bid'
h: number
mark?: number
}
function fmtNotional(n: number): string {
if (n >= 1e9) return `$${(n / 1e9).toFixed(2)}B`
if (n >= 1e6) return `$${(n / 1e6).toFixed(2)}M`
if (n >= 1e3) return `$${(n / 1e3).toFixed(1)}K`
return `$${n.toFixed(0)}`
}
function Row({ px, sz, cum, maxCum, side, h, mark }: RowProps) {
const pct = Math.min(100, (cum / maxCum) * 100)
const color = side === 'ask' ? 'var(--tm-dn)' : 'var(--tm-up)'
// bold cumulative-depth bar, saturated toward the edge, that animates its
// width as the book updates (the live "growing ladder" effect)
const bar = side === 'ask'
? 'linear-gradient(to left, rgba(214,67,58,0.36), rgba(214,67,58,0.05))'
: 'linear-gradient(to left, rgba(46,139,87,0.36), rgba(46,139,87,0.05))'
const isMark = mark != null && px === mark
// this Row instance is keyed by price, so these refs persist across updates —
// we flash green/red only when THIS level's size actually changes, and keep
// the direction class fixed until the next change so the animation isn't cut
// short by the 60fps re-renders.
const prevSz = useRef(sz)
const dirRef = useRef('')
if (sz !== prevSz.current) {
dirRef.current = sz > prevSz.current ? 'ob-up' : 'ob-dn'
prevSz.current = sz
}
return (
<div style={{ position: 'relative', height: h, display: 'flex', alignItems: 'center' }}>
{/* per-row flash overlay — keyed by size so it remounts (replays the
animation) exactly when this level's size changes */}
<div key={sz} className={dirRef.current} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }} />
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: `${pct}%`, background: bar, transition: 'width 0.16s ease-out' }} />
<div style={{ position: 'relative', display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4, width: '100%', alignItems: 'center' }}>
<span style={{ color, fontWeight: isMark ? 700 : 500 }}>
{isMark ? '▸ ' : ''}{fmtPx(px)}
</span>
<span style={{ textAlign: 'right', color: 'var(--tm-ink)' }}>{fmtSz(sz)}</span>
<span style={{ textAlign: 'right', color: 'var(--tm-muted)' }}>{fmtNotional(cum * px)}</span>
</div>
</div>
)
}
export default OrderBook

View File

@@ -0,0 +1,383 @@
import { useMemo } from 'react'
import type { Position } from '../../types'
/**
* RiskRadar renders derived risk telemetry for the live trading book — long /
* short exposure split, leverage usage vs config cap, margin utilization,
* single-name concentration, max drawdown and position count — as a dense stack
* of self-explanatory gauge rows. Each row reads as: bilingual label · value +
* unit · a thin gauge bar · and a one-glance verdict tag. Cream-themed
* Bloomberg/terminal cockpit.
*
* Every value is DERIVED from real props. No synthetic or random data; missing
* inputs collapse to 0 and divides are guarded.
*/
const C_AMBER = '#c8860b' // escalation tint between green and red
function fmtUsd(n: number): string {
const a = Math.abs(n)
const sign = n < 0 ? '-' : ''
if (a >= 1e6) return `${sign}$${(a / 1e6).toFixed(2)}M`
if (a >= 1e3) return `${sign}$${(a / 1e3).toFixed(1)}K`
return `${sign}$${a.toFixed(0)}`
}
function pct(n: number): string {
return `${n.toFixed(1)}%`
}
function isLong(side: string): boolean {
return (side || '').toLowerCase() === 'long'
}
// margin utilization escalates green → amber → red as the book fills up
function utilColor(p: number): string {
if (p > 80) return 'var(--tm-dn)'
if (p >= 50) return C_AMBER
return 'var(--tm-up)'
}
interface RiskRadarProps {
positions?: Position[]
account?: { total_equity?: number; total_unrealized_profit?: number; margin_used_pct?: number } | null
config?: { btc_eth_leverage?: number; altcoin_leverage?: number; max_positions?: number } | null
fullStats?: { max_drawdown_pct?: number; profit_factor?: number; sharpe_ratio?: number; win_rate?: number } | null
}
export function RiskRadar({ positions, account, config, fullStats }: RiskRadarProps) {
const pos = positions ?? []
const m = useMemo(() => {
const equity = account?.total_equity ?? 0
let longNotional = 0
let shortNotional = 0
let levSum = 0
let levCount = 0
let maxLev = 0
let marginSum = 0
let topNotional = 0
for (const p of pos) {
const px = p.mark_price || p.entry_price || 0
const notional = Math.abs(p.quantity || 0) * px
if (isLong(p.side)) longNotional += notional
else shortNotional += notional
const lev = p.leverage || 0
if (lev > 0) {
levSum += lev
levCount += 1
if (lev > maxLev) maxLev = lev
}
marginSum += p.margin_used || 0
if (notional > topNotional) topNotional = notional
}
const totalNotional = longNotional + shortNotional
const netNotional = longNotional - shortNotional
const longShare = totalNotional > 0 ? (longNotional / totalNotional) * 100 : 0
const shortShare = totalNotional > 0 ? (shortNotional / totalNotional) * 100 : 0
const avgLev = levCount > 0 ? levSum / levCount : 0
const configMax = Math.max(config?.btc_eth_leverage ?? 0, config?.altcoin_leverage ?? 0)
const levUse = configMax > 0 ? Math.min(100, (avgLev / configMax) * 100) : 0
const marginPct =
account?.margin_used_pct != null
? account.margin_used_pct
: equity > 0
? (marginSum / equity) * 100
: 0
const concentration = totalNotional > 0 ? (topNotional / totalNotional) * 100 : 0
const ddFrac = fullStats?.max_drawdown_pct ?? 0
const drawdown = ddFrac * 100
const count = pos.length
const maxPositions = config?.max_positions ?? 0
const countUse = maxPositions > 0 ? Math.min(100, (count / maxPositions) * 100) : 0
const upnl = account?.total_unrealized_profit ?? 0
return {
longNotional,
shortNotional,
netNotional,
longShare,
shortShare,
totalNotional,
avgLev,
maxLev,
configMax,
levUse,
marginPct,
concentration,
drawdown,
count,
maxPositions,
countUse,
upnl,
}
}, [pos, account, config, fullStats])
const hasData = pos.length > 0 || account != null
if (!hasData) {
return <div className="tm-sc" style={{ padding: '16px 0' }}>No live risk data.</div>
}
// ── one-glance verdicts ──────────────────────────────────────────────
// Net exposure bias: Long-lean / Short-lean / Balanced by the long-share spread around 50%.
const biasSkew = m.longShare - m.shortShare
const exposureTag: Verdict =
m.totalNotional === 0
? { text: 'Flat', tone: 'muted' }
: biasSkew > 15
? { text: 'Long-lean', tone: 'up' }
: biasSkew < -15
? { text: 'Short-lean', tone: 'dn' }
: { text: 'Balanced', tone: 'ink' }
// Leverage: Safe / High / Risky by avg vs cap.
const levTag: Verdict =
m.configMax === 0 || m.avgLev === 0
? { text: '—', tone: 'muted' }
: m.levUse > 80
? { text: 'Risky', tone: 'dn' }
: m.levUse >= 50
? { text: 'High', tone: 'amber' }
: { text: 'Safe', tone: 'up' }
// Margin used: Ample / Tight / Risky.
const marginTag: Verdict =
m.marginPct > 80
? { text: 'Risky', tone: 'dn' }
: m.marginPct >= 50
? { text: 'Tight', tone: 'amber' }
: { text: 'Ample', tone: 'up' }
// Concentration: Spread / Concentrated.
const concTag: Verdict =
m.totalNotional === 0
? { text: '—', tone: 'muted' }
: m.concentration >= 35
? { text: 'Concentrated', tone: 'amber' }
: { text: 'Spread', tone: 'up' }
// Drawdown: Calm / Caution / Deep by depth.
const ddTag: Verdict =
m.drawdown <= 0
? { text: 'Calm', tone: 'up' }
: m.drawdown >= 20
? { text: 'Deep', tone: 'dn' }
: { text: 'Caution', tone: 'amber' }
// Positions: Room / Full.
const countTag: Verdict =
m.maxPositions === 0
? { text: `${m.count}`, tone: 'muted' }
: m.count >= m.maxPositions
? { text: 'Full', tone: 'amber' }
: { text: 'Room', tone: 'up' }
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
{/* header */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 1 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Risk radar</span>
<span
className="tm-sc"
style={{ marginLeft: 'auto', color: m.totalNotional > 0 ? 'var(--tm-up)' : 'var(--tm-muted)' }}
>
{m.totalNotional > 0 ? '● live' : '○ flat'}
</span>
</div>
<div className="tm-sc" style={{ fontSize: 9, marginBottom: 8 }}>
Risk radar · live position-risk check
</div>
{/* Net exposure — diverging long/short split, the visual centerpiece */}
<div style={{ marginBottom: 9, paddingBottom: 9, borderBottom: '1px solid var(--tm-hair)' }}>
<div style={{ display: 'flex', alignItems: 'baseline', marginBottom: 4 }}>
<Label zh="Net exposure" en="NET EXPOSURE" />
<Tag verdict={exposureTag} />
<span className="tm-mono" style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--tm-ink)' }}>
long {pct(m.longShare)}
<span style={{ color: 'var(--tm-muted)' }}> / </span>
short {pct(m.shortShare)}
</span>
</div>
<div style={{ display: 'flex', height: 7, background: 'var(--tm-hair)', overflow: 'hidden' }}>
<div style={{ width: `${m.longShare}%`, background: 'var(--tm-up)' }} />
<div style={{ width: `${m.shortShare}%`, background: 'var(--tm-dn)' }} />
</div>
<div className="tm-mono" style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9, marginTop: 3 }}>
<span style={{ color: 'var(--tm-up)' }}>long {fmtUsd(m.longNotional)}</span>
<span style={{ color: 'var(--tm-ink-2)' }}>
net <b style={{ color: m.netNotional >= 0 ? 'var(--tm-up)' : 'var(--tm-dn)' }}>{fmtUsd(m.netNotional)}</b>
</span>
<span style={{ color: 'var(--tm-dn)' }}>short {fmtUsd(m.shortNotional)}</span>
</div>
</div>
{/* gauge rows */}
<GaugeRow
zh="Leverage"
en="LEVERAGE"
value={`${m.avgLev.toFixed(1)}× avg`}
sub={`/ ${m.maxLev > 0 ? `${m.maxLev.toFixed(0)}×` : '—'} peak · ${m.configMax > 0 ? `${m.configMax}×` : '—'} cap`}
fill={m.levUse}
color={levTag.tone === 'dn' ? 'var(--tm-dn)' : levTag.tone === 'amber' ? C_AMBER : 'var(--tm-up)'}
verdict={levTag}
/>
<GaugeRow
zh="Margin used"
en="MARGIN USED"
value={pct(m.marginPct)}
sub="of equity"
fill={Math.min(100, Math.max(0, m.marginPct))}
color={utilColor(m.marginPct)}
verdict={marginTag}
/>
<GaugeRow
zh="Concentration"
en="CONCENTRATION"
value={pct(m.concentration)}
sub="top-position share"
fill={m.concentration}
color={concTag.tone === 'amber' ? C_AMBER : 'var(--tm-up)'}
verdict={concTag}
/>
<GaugeRow
zh="Drawdown"
en="MAX DRAWDOWN"
value={`-${pct(m.drawdown)}`}
sub="peak drawdown"
fill={Math.min(100, m.drawdown)}
color="var(--tm-red)"
verdict={ddTag}
valueColor="var(--tm-dn)"
/>
<GaugeRow
zh="Positions"
en="POSITIONS"
value={m.maxPositions > 0 ? `${m.count} / ${m.maxPositions}` : `${m.count}`}
sub="held / cap"
fill={m.maxPositions > 0 ? m.countUse : 0}
color={countTag.tone === 'amber' ? C_AMBER : 'var(--tm-up)'}
verdict={countTag}
/>
{/* unrealized PnL footer */}
<div
style={{
display: 'flex',
alignItems: 'baseline',
marginTop: 8,
paddingTop: 7,
borderTop: '1px solid var(--tm-hair)',
}}
>
<Label zh="Unrealized PnL" en="UNREALIZED PNL" />
<span
className="tm-mono"
style={{ marginLeft: 'auto', fontSize: 13, fontWeight: 700, color: m.upnl >= 0 ? 'var(--tm-up)' : 'var(--tm-dn)' }}
>
{m.upnl >= 0 ? '+' : ''}{fmtUsd(m.upnl)}
</span>
</div>
</div>
)
}
// ── verdict tag ────────────────────────────────────────────────────────
type Tone = 'up' | 'dn' | 'amber' | 'ink' | 'muted'
interface Verdict {
text: string
tone: Tone
}
function toneColor(tone: Tone): string {
switch (tone) {
case 'up':
return 'var(--tm-up)'
case 'dn':
return 'var(--tm-dn)'
case 'amber':
return C_AMBER
case 'ink':
return 'var(--tm-ink)'
default:
return 'var(--tm-muted)'
}
}
function Tag({ verdict }: { verdict: Verdict }) {
const c = toneColor(verdict.tone)
return (
<span
style={{
marginLeft: 6,
padding: '0 4px',
fontSize: 9,
lineHeight: '13px',
letterSpacing: '0.08em',
color: c,
border: `1px solid ${c}`,
borderRadius: 2,
}}
>
{verdict.text}
</span>
)
}
// ── bilingual label block ──────────────────────────────────────────────
function Label({ zh, en }: { zh: string; en: string }) {
return (
<span style={{ display: 'inline-flex', flexDirection: 'column', lineHeight: 1.2 }}>
<span style={{ fontSize: 11, color: 'var(--tm-ink)', fontWeight: 600 }}>{zh}</span>
<span className="tm-sc" style={{ fontSize: 8, letterSpacing: '0.12em' }}>{en}</span>
</span>
)
}
interface GaugeRowProps {
zh: string
en: string
value: string
sub?: string
fill: number
color: string
verdict: Verdict
valueColor?: string
}
function GaugeRow({ zh, en, value, sub, fill, color, verdict, valueColor }: GaugeRowProps) {
const w = Math.min(100, Math.max(0, fill))
return (
<div style={{ marginBottom: 9 }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
<Label zh={zh} en={en} />
<Tag verdict={verdict} />
<span
className="tm-mono"
style={{ marginLeft: 'auto', fontSize: 12, fontWeight: 600, color: valueColor ?? 'var(--tm-ink)' }}
>
{value}
</span>
</div>
<div style={{ height: 5, background: 'var(--tm-hair)', overflow: 'hidden' }}>
<div style={{ width: `${w}%`, height: '100%', background: color, transition: 'width 0.2s ease-out' }} />
</div>
{sub && (
<div className="tm-sc" style={{ fontSize: 8, marginTop: 2 }}>{sub}</div>
)}
</div>
)
}
export default RiskRadar

View File

@@ -0,0 +1,192 @@
import { useMemo } from 'react'
import type { SignalRankItem } from '../../lib/api/data'
/**
* SignalMatrix renders the vergex (claw402) signal ranking as a high-density
* heatmap grid — one cell per symbol, colored by directional bias (green =
* bullish, red = bearish, muted = neutral) with intensity scaled by the signal
* score. Rank 1 is the strongest signal. Cream-themed Bloomberg/terminal feel.
*
* Real ranked data only — no synthetic signals.
*/
function baseSymbol(raw: string): string {
return raw
.toUpperCase()
.replace(/^XYZ:/, '')
.replace(/(USDT|USDC|USD)$/, '')
}
type Bias = 'bullish' | 'bearish' | 'neutral'
function normBias(raw: string): Bias {
const b = (raw || '').toLowerCase()
if (b === 'bullish') return 'bullish'
if (b === 'bearish') return 'bearish'
return 'neutral'
}
// solid bias color for the left accent + swatches
const ACCENT: Record<Bias, string> = {
bullish: 'var(--tm-up)',
bearish: 'var(--tm-dn)',
neutral: 'var(--tm-muted)',
}
// background wash, intensity-scaled (alpha 0.10 → 0.42)
function washFor(bias: Bias, intensity: number): string {
const a = 0.1 + Math.max(0, Math.min(1, intensity)) * 0.32
if (bias === 'bullish') return `rgba(46,139,87,${a.toFixed(3)})`
if (bias === 'bearish') return `rgba(214,67,58,${a.toFixed(3)})`
return 'rgba(138,132,120,0.10)'
}
interface Cell {
rank: number
symbol: string
bias: Bias
score: number
intensity: number
}
interface SignalMatrixProps {
items?: SignalRankItem[]
max?: number
/** currently-selected base symbol (drives the liq map + order book) */
active?: string
/** click a cell to switch the active instrument */
onSelect?: (symbol: string) => void
}
export function SignalMatrix({ items, max = 36, active, onSelect }: SignalMatrixProps) {
const view = useMemo(() => {
const raw = items ?? []
const sorted = [...raw].sort((a, b) => a.rank - b.rank).slice(0, max)
if (!sorted.length) return { cells: [] as Cell[], bull: 0, bear: 0, neut: 0 }
const scores = sorted.map((s) => s.score)
const min = Math.min(...scores)
const span = Math.max(...scores) - min
const n = sorted.length
let bull = 0
let bear = 0
let neut = 0
const cells: Cell[] = sorted.map((s, i) => {
const bias = normBias(s.bias)
if (bias === 'bullish') bull += 1
else if (bias === 'bearish') bear += 1
else neut += 1
// intensity by normalized score; if scores are uniform, fall back to rank
// (top ranks brighter, descending across the slice).
const intensity = span > 0 ? (s.score - min) / span : n > 1 ? 1 - i / (n - 1) : 1
return { rank: s.rank, symbol: s.symbol, bias, score: s.score, intensity }
})
return { cells, bull, bear, neut }
}, [items, max])
if (!view.cells.length) {
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
<Head />
<div className="tm-sc">No signal data (claw402).</div>
</div>
)
}
return (
<div style={{ fontFamily: 'var(--tm-mono)' }}>
<Head />
{/* legend */}
<div
className="tm-sc"
style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 6, fontSize: 9 }}
>
<Swatch c="var(--tm-up)" label="Bullish" />
<Swatch c="var(--tm-dn)" label="Bearish" />
<Swatch c="var(--tm-muted)" label="Neutral" />
{onSelect && <span style={{ color: 'var(--tm-red)' }}>click to switch </span>}
<span style={{ marginLeft: 'auto' }}>{view.cells.length} signals</span>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(64px, 1fr))',
gap: 3,
}}
>
{view.cells.map((c) => {
const base = baseSymbol(c.symbol)
const isActive = !!active && base === active.toUpperCase()
return (
<div
key={`${c.rank}-${c.symbol}`}
title={`${base} · #${c.rank} · ${c.bias} · ${c.score} — click to switch`}
onClick={onSelect ? () => onSelect(base) : undefined}
style={{
padding: '4px 5px',
background: washFor(c.bias, c.intensity),
border: isActive ? '1px solid var(--tm-red)' : '1px solid var(--tm-hair)',
borderLeft: `2px solid ${ACCENT[c.bias]}`,
outline: isActive ? '1px solid var(--tm-red)' : 'none',
boxShadow: isActive ? 'inset 0 0 0 1px var(--tm-red)' : 'none',
lineHeight: 1.15,
overflow: 'hidden',
cursor: onSelect ? 'pointer' : 'default',
}}
>
<div
className="tm-mono"
style={{
fontSize: 10,
fontWeight: 700,
color: 'var(--tm-ink)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{baseSymbol(c.symbol)}
</div>
<div
className="tm-sc"
style={{ fontSize: 8, letterSpacing: '0.08em', marginTop: 1 }}
>
#{c.rank} · {fmtScore(c.score)}
</div>
</div>
)
})}
</div>
</div>
)
}
function fmtScore(n: number): string {
if (!Number.isFinite(n)) return '—'
if (Math.abs(n) >= 100) return n.toFixed(0)
if (Math.abs(n) >= 10) return n.toFixed(1)
return n.toFixed(2)
}
function Head() {
return (
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Signal matrix</span>
<span className="tm-sc">Signal matrix · vergex</span>
</div>
)
}
function Swatch({ c, label }: { c: string; label: string }) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
<span style={{ width: 8, height: 8, background: c, display: 'inline-block' }} />
{label}
</span>
)
}
export default SignalMatrix

View File

@@ -0,0 +1,566 @@
import { useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import type { CSSProperties } from 'react'
import useSWR from 'swr'
import { api } from '../../lib/api'
import type {
SystemStatus,
AccountInfo,
Position,
DecisionRecord,
TraderInfo,
} from '../../types'
import { OrchestrationTopology } from './OrchestrationTopology'
import { OrderBook } from './OrderBook'
import { LiquidationMap } from './LiquidationMap'
import { KlineChart } from './KlineChart'
import { ExecutionLog } from './ExecutionLog'
import { SignalMatrix } from './SignalMatrix'
import { RiskRadar } from './RiskRadar'
// crypto majors trade on the Hyperliquid main dex (no hip3 cost/liq heatmap);
// everything else in the universe is an xyz-dex synthetic market that does.
const CRYPTO_MAJORS = new Set([
'BTC', 'ETH', 'SOL', 'HYPE', 'BNB', 'XRP', 'DOGE', 'AVAX', 'LINK', 'SUI', 'APT', 'ARB', 'OP',
'TON', 'ADA', 'TRX', 'LTC', 'BCH', 'NEAR', 'INJ', 'SEI', 'TIA', 'PEPE', 'WIF', 'BONK', 'AAVE',
'UNI', 'ENA', 'ONDO', 'JUP', 'PENDLE', 'KPEPE', 'ZEC', 'XPL', 'LIT',
])
// fixed height for the three row-1 panels so the row stays balanced at any width
const ROW1_H = 500
import { FlowMarkets } from './FlowMarkets'
import './terminal.css'
interface TerminalDashboardProps {
selectedTrader?: TraderInfo
traders?: TraderInfo[]
selectedTraderId?: string
onTraderSelect: (traderId: string) => void
status?: SystemStatus
account?: AccountInfo
positions?: Position[]
decisions?: DecisionRecord[]
}
function fmtUsd(n: number | undefined, signed = false): string {
if (n == null || Number.isNaN(n)) return '—'
const sign = signed && n > 0 ? '+' : n < 0 ? '-' : ''
return `${sign}$${Math.abs(n).toLocaleString('en-US', { maximumFractionDigits: 2 })}`
}
function fmtPct(n: number | undefined): string {
if (n == null || Number.isNaN(n)) return '—'
return `${n >= 0 ? '+' : ''}${n.toFixed(2)}%`
}
function baseLabel(raw?: string): string {
if (!raw) return ''
return raw.toUpperCase().replace(/^XYZ:/, '').replace(/[-_]/g, '').replace(/(USDT|USDC|USD)$/, '')
}
function parseScanMinutes(scan?: string): number {
if (!scan) return 15
const m = scan.match(/(\d+)\s*m/i)
if (m) return parseInt(m[1], 10)
const h = scan.match(/(\d+)\s*h/i)
if (h) return parseInt(h[1], 10) * 60
const n = parseInt(scan, 10)
return Number.isFinite(n) && n > 0 ? n : 15
}
function fmtTime(raw?: string | number): string {
if (raw == null || raw === '') return ''
let n = typeof raw === 'number' ? raw : Number(raw)
if (Number.isFinite(n)) {
if (n < 1e12) n *= 1000
return new Date(n).toLocaleString('en-GB', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
}
const d = new Date(raw as string)
return Number.isNaN(d.getTime()) ? '' : d.toLocaleString('en-GB', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
}
function useTick(ms = 1000) {
const [, set] = useState(0)
useEffect(() => {
const id = setInterval(() => set((n) => n + 1), ms)
return () => clearInterval(id)
}, [ms])
}
export function TerminalDashboard({
selectedTrader,
traders,
selectedTraderId,
onTraderSelect,
status,
account,
positions,
decisions,
}: TerminalDashboardProps) {
const traderId = selectedTrader?.trader_id || selectedTraderId
useTick(1000)
const clock = new Date().toLocaleTimeString('en-GB', { hour12: false })
const { data: fullStats } = useSWR(
traderId ? ['full-stats', traderId] : null,
() => api.getFullStats(traderId!, true),
{ refreshInterval: 30000, shouldRetryOnError: false }
)
const { data: equity } = useSWR(
traderId ? ['equity-history', traderId] : null,
() => api.getEquityHistory(traderId!, true),
{ refreshInterval: 30000, shouldRetryOnError: false }
)
const { data: history } = useSWR(
traderId ? ['pos-history', traderId] : null,
() => api.getPositionHistory(traderId!, 50, true),
{ refreshInterval: 60000, shouldRetryOnError: false }
)
const { data: config } = useSWR(
traderId ? ['trader-config', traderId] : null,
() => api.getTraderConfig(traderId!, true),
{ refreshInterval: 120000, shouldRetryOnError: false }
)
const latest = decisions && decisions.length > 0 ? decisions[0] : undefined
const candidateCoins = latest?.candidate_coins ?? []
const { data: flow } = useSWR(
traderId ? ['flow-markets', traderId] : null,
() => api.getFlowMarkets(selectedTrader?.ai_model, 'mainnet', '1h', 50, true),
// paid x402 endpoint — poll slowly (5m) to conserve claw402 funds; the
// topology beam animation is client-side and stays fast regardless
{ refreshInterval: 300000, shouldRetryOnError: false }
)
const flowItems = flow?.data?.inflow ?? []
const { data: signalRank } = useSWR(
traderId ? ['signal-rank', traderId] : null,
() => api.getSignalRanking(selectedTrader?.ai_model, 'mainnet', 'all', 30, true),
// paid x402 endpoint — poll slowly (5m) to conserve claw402 funds
{ refreshInterval: 300000, shouldRetryOnError: false }
)
// Both the cost/liq map and the order book follow this symbol so they stay in
// sync. The heatmap only covers hip3_perp synthetic markets, so we pick a
// synthetic (non-crypto) the bot trades — preferring the BUSIEST one (most
// 1h trades, per flow-markets) so the shared order book ticks as fast as
// possible. Falls back to any held synthetic, then SP500.
const heatmapSymbol = useMemo(() => {
const held = new Set(
[...(positions ?? []).map((p) => p.symbol), ...candidateCoins]
.map(baseLabel)
.filter((b) => b && !CRYPTO_MAJORS.has(b)),
)
const synthByActivity = flowItems
.map((i) => ({ b: baseLabel(i.symbol), trades: i.trades || 0 }))
.filter((x) => x.b && !CRYPTO_MAJORS.has(x.b))
.sort((a, b) => b.trades - a.trades)
const busiestHeld = synthByActivity.find((x) => held.has(x.b))
if (busiestHeld) return busiestHeld.b
if (held.size) return [...held][0]
if (synthByActivity.length) return synthByActivity[0].b
return 'SP500'
}, [positions, candidateCoins, flowItems])
// user can click a signal-matrix cell to drive both the cost/liq map and the
// order book. Default to the instrument the bot is ACTUALLY holding (first
// open position, else this cycle's first candidate) so the price panels match
// the real traded symbol; only fall back to the busiest synthetic if the bot
// holds nothing.
const [selectedSym, setSelectedSym] = useState<string | null>(null)
const defaultSym = useMemo(() => {
// the bot's actual first open position (else this cycle's first candidate);
// every market — synthetic or crypto — now has a cost/liq heatmap, so no
// need to prefer one type. Falls back to the busiest synthetic if flat.
const heldBases = [...(positions ?? []).map((p) => p.symbol), ...candidateCoins].map(baseLabel).filter(Boolean)
return heldBases[0] || heatmapSymbol || 'SP500'
}, [positions, candidateCoins, heatmapSymbol])
const activeSym = (selectedSym || defaultSym).toUpperCase()
const pnl = account?.total_pnl ?? 0
const pnlPct = account?.total_pnl_pct ?? 0
const up = pnl >= 0
const running = status?.is_running
// direction per symbol — priority: AI's actual decision > signal bias >
// net flow > prevailing market majority (never blindly default to long).
const dirFor = useMemo(() => {
const dec = new Map<string, 'long' | 'short'>()
;(latest?.decisions ?? []).forEach((d) => {
const b = baseLabel(d.symbol)
if (d.action === 'open_long' || d.action === 'close_short') dec.set(b, 'long')
else if (d.action === 'open_short' || d.action === 'close_long') dec.set(b, 'short')
})
const sig = new Map<string, 'long' | 'short'>()
let bull = 0
let bear = 0
;(signalRank?.items ?? []).forEach((s) => {
const b = baseLabel(s.symbol)
const bias = (s.bias || '').toLowerCase()
if (bias === 'bearish') { sig.set(b, 'short'); bear++ }
else if (bias === 'bullish') { sig.set(b, 'long'); bull++ }
})
const fl = new Map<string, 'long' | 'short'>()
;(flow?.data?.inflow ?? []).forEach((i) => fl.set(baseLabel(i.symbol), 'long'))
;(flow?.data?.outflow ?? []).forEach((i) => fl.set(baseLabel(i.symbol), 'short'))
const majority: 'long' | 'short' = bear > bull ? 'short' : 'long'
return (sym: string): 'long' | 'short' => {
const b = baseLabel(sym)
return dec.get(b) ?? sig.get(b) ?? fl.get(b) ?? majority
}
}, [latest, signalRank, flow])
const scanMin = config?.scan_interval_minutes || parseScanMinutes(status?.scan_interval)
const nextCycleMs = useMemo(() => {
if (!latest?.timestamp) return null
return new Date(latest.timestamp).getTime() + scanMin * 60_000
}, [latest?.timestamp, scanMin])
let countdown = '—'
if (nextCycleMs) {
const ms = nextCycleMs - Date.now()
if (ms <= 0) countdown = 'due now'
else {
const s = Math.floor(ms / 1000)
countdown = `${Math.floor(s / 60)}m ${s % 60}s`
}
}
const equityPath = useMemo(() => {
if (!equity || equity.length < 2) return null
const vals = equity.map((e: { total_equity: number }) => e.total_equity)
const min = Math.min(...vals)
const max = Math.max(...vals)
const span = max - min || 1
const W = 680
const H = 80
return vals
.map((v: number, i: number) => {
const x = (i / (vals.length - 1)) * W
const y = H - ((v - min) / span) * (H - 10) - 5
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`
})
.join(' ')
}, [equity])
const recentTrades = (history?.positions ?? []).slice(0, 8)
const symbolStats = useMemo(
() => (history?.symbol_stats ?? []).slice().sort((a, b) => b.total_trades - a.total_trades).slice(0, 6),
[history]
)
const maxSymTrades = symbolStats.reduce((m, s) => Math.max(m, s.total_trades), 1)
const sc: CSSProperties = { padding: '10px 14px' }
const cellBorder = '1px solid var(--tm-hair)'
// Portal the trader selector + run status into the global nav so the app has
// a single top bar (no separate dashboard titlebar).
const [navSlot, setNavSlot] = useState<HTMLElement | null>(null)
useEffect(() => {
setNavSlot(document.getElementById('dash-header-slot'))
}, [])
return (
<div className="nofx-terminal" style={{ minHeight: '100vh', padding: 0 }}>
{/* centered, capped content column — no border (keeps it from feeling
embedded) but bounded so the aspect-ratio SVGs don't balloon on wide screens */}
{navSlot &&
createPortal(
<span className="nofx-terminal" style={{ background: 'transparent', display: 'flex', alignItems: 'center', gap: 12, marginLeft: 16, paddingLeft: 16, borderLeft: '1px solid rgba(26,24,19,0.15)', fontSize: 11 }}>
<span className="tm-sc" style={{ color: 'var(--tm-muted)' }}>orchestration</span>
{traders && traders.length > 0 && (
<select value={traderId || ''} onChange={(e) => onTraderSelect(e.target.value)} className="tm-mono"
style={{ background: 'var(--tm-panel)', color: 'var(--tm-ink)', border: '1px solid var(--tm-hair)', borderRadius: 0, fontSize: 11, padding: '3px 6px' }}>
{traders.map((t) => (<option key={t.trader_id} value={t.trader_id} style={{ color: '#111' }}>{t.trader_name}</option>))}
</select>
)}
<span style={{ color: running ? 'var(--tm-up)' : 'var(--tm-muted)' }}>{running ? '● running' : '○ stopped'}</span>
<span className="tm-sc" style={{ color: 'var(--tm-muted)' }}>cycle</span><span className="tm-mono" style={{ color: 'var(--tm-ink)' }}>{status?.call_count ?? '—'}</span>
<span className="tm-px" style={{ fontSize: 12, color: 'var(--tm-ink)' }}>{clock}</span>
</span>,
navSlot,
)}
<div className="tm-box" style={{ maxWidth: 1280, margin: '0 auto', border: 'none' }}>
{/* config / identity strip — first row, flows directly under the global nav */}
<div className="tm-mono" style={{ display: 'flex', gap: 16, padding: '6px 14px', fontSize: 11, color: 'var(--tm-ink-2)', flexWrap: 'wrap' }}>
<span style={{ fontWeight: 500 }}>{selectedTrader?.trader_name ?? 'NOFX'}</span>
<span><span className="tm-sc">model </span>{(() => {
const raw = config?.ai_model || status?.ai_model || ''
if (!raw) return '—'
if (/claw402/i.test(raw)) return 'CLAW402'
return raw.length > 16 ? raw.slice(0, 16).toUpperCase() : raw.toUpperCase()
})()}</span>
<span><span className="tm-sc">strategy </span>{config?.strategy_name || selectedTrader?.strategy_name || '—'}</span>
<span><span className="tm-sc">lev </span>{config?.btc_eth_leverage ?? '—'}× / {config?.altcoin_leverage ?? '—'}×</span>
<span><span className="tm-sc">scan </span>{scanMin}m</span>
<span><span className="tm-sc">universe </span>{candidateCoins.length}</span>
<span><span className="tm-sc">positions </span>{positions?.length ?? 0}</span>
<span style={{ marginLeft: 'auto' }}><span className="tm-sc">next cycle </span>{countdown}</span>
</div>
<div className="tm-rule" />
{/* metric row */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)' }}>
{[
{ l: 'Equity', v: fmtUsd(account?.total_equity), c: 'var(--tm-ink)' },
{ l: 'Total P/L', v: `${fmtUsd(pnl, true)} (${fmtPct(pnlPct)})`, c: up ? 'var(--tm-up)' : 'var(--tm-dn)' },
{ l: 'Win rate', v: fullStats != null ? `${fullStats.win_rate.toFixed(1)}%` : '—', c: 'var(--tm-ink)' },
{ l: 'Profit factor', v: fullStats != null ? fullStats.profit_factor.toFixed(2) : '—', c: 'var(--tm-ink)' },
{ l: 'Max drawdown', v: fullStats != null ? `-${(fullStats.max_drawdown_pct * 100).toFixed(1)}%` : '—', c: 'var(--tm-dn)' },
].map((m, i) => (
<div key={m.l} style={{ padding: '12px 14px', borderRight: i < 4 ? cellBorder : 'none' }}>
<div className="tm-sc">{m.l}</div>
<div className="tm-mono" style={{ fontSize: 17, fontWeight: 500, color: m.c, marginTop: 3 }}>{m.v}</div>
</div>
))}
</div>
<div className="tm-rule" />
{/* trades summary */}
{fullStats != null && (
<>
<div className="tm-mono" style={{ display: 'flex', gap: 18, padding: '6px 14px', fontSize: 11, color: 'var(--tm-ink-2)', flexWrap: 'wrap' }}>
<span className="tm-sc">trades <b style={{ color: 'var(--tm-ink)' }}>{fullStats.total_trades}</b></span>
<span className="tm-sc tm-up">win {fullStats.win_trades}</span>
<span className="tm-sc tm-dn">loss {fullStats.loss_trades}</span>
<span className="tm-sc">sharpe <b style={{ color: 'var(--tm-ink)' }}>{fullStats.sharpe_ratio.toFixed(2)}</b></span>
<span className="tm-sc">avg win/loss <b style={{ color: 'var(--tm-ink)' }}>{fullStats.avg_win.toFixed(2)}/{fullStats.avg_loss.toFixed(2)}</b></span>
<span className="tm-sc">fees <b style={{ color: 'var(--tm-ink)' }}>{fmtUsd(fullStats.total_fee)}</b></span>
</div>
<div className="tm-rule" />
</>
)}
{/* ── row 1: cost/liq map · live L2 order book · signal matrix (instrument selector)
all three columns are locked to one fixed height so the row is always
balanced; the K-line flexes to fill any remaining space ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.1fr) minmax(0,0.95fr) minmax(0,1.05fr)' }}>
<div style={{ ...sc, borderRight: cellBorder, height: ROW1_H, overflow: 'hidden' }}>
{/* cost/liq heatmap works for both synthetic (hip3_perp) and crypto
(perp) markets — pass the likely marketType; the component falls
back to the other one if the guess is wrong */}
<LiquidationMap
symbol={activeSym}
marketType={CRYPTO_MAJORS.has(activeSym) ? 'perp' : 'hip3_perp'}
height={ROW1_H - 130}
/>
</div>
<div style={{ ...sc, borderRight: cellBorder, height: ROW1_H, overflow: 'hidden' }}>
<OrderBook symbol={activeSym} markPrice={positions?.find((p) => baseLabel(p.symbol) === activeSym)?.entry_price} />
</div>
<div style={{ ...sc, height: ROW1_H, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<SignalMatrix items={signalRank?.items} active={activeSym} onSelect={setSelectedSym} />
{/* the live K-line always sits under the selector and flexes to fill */}
<div className="tm-rule" style={{ margin: '10px 0 8px' }} />
<div style={{ flex: 1, minHeight: 0 }}>
<KlineChart symbol={activeSym} fill />
</div>
</div>
</div>
<div className="tm-rule" />
{/* orchestration topology — second row, full width (the agent workflow) */}
<div style={sc}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 4 }}>
<span className="tm-px" style={{ fontSize: 12 }}>Orchestration topology</span>
<span className="tm-sc">Orchestration topology · net inflow signal execute hold</span>
</div>
<OrchestrationTopology
layers={[
{
key: 'flow',
title: 'FLOW',
zh: 'flow',
items: [
...(flow?.data?.inflow ?? []).map((i) => ({ symbol: i.symbol, dir: 'long' as const })),
...(flow?.data?.outflow ?? []).map((i) => ({ symbol: i.symbol, dir: 'short' as const })),
],
},
{
key: 'signal',
title: 'SIGNAL',
zh: 'signal',
items: (signalRank?.items ?? []).map((s) => ({
symbol: s.symbol,
dir: (s.bias || '').toLowerCase() === 'bearish' ? ('short' as const) : ('long' as const),
})),
},
{
// every candidate the AI actually judged this cycle (its full decision set)
key: 'decision',
title: 'DECISION',
zh: 'decision',
items: candidateCoins.map((c) => ({ symbol: c, dir: dirFor(c) })),
},
{
// executed & live: every open position is an executed order, so
// EXECUTE mirrors the live book (this cycle's fills plus anything
// still open from prior cycles) and flows straight into HOLD
key: 'exec',
title: 'EXECUTE',
zh: 'execute',
items: (positions ?? []).map((p) => ({
symbol: p.symbol,
dir: (p.side || '').toLowerCase().includes('short') ? ('short' as const) : ('long' as const),
})),
},
{
key: 'hold',
title: 'HOLD',
zh: 'hold',
items: (positions ?? []).map((p) => ({
symbol: p.symbol,
dir: (p.side || '').toLowerCase().includes('short') ? ('short' as const) : ('long' as const),
})),
},
]}
/>
</div>
<div className="tm-rule" />
{/* ── row 3: execution log · risk radar · recent trades ── */}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.1fr) minmax(0,1fr) minmax(0,1fr)' }}>
<div style={{ ...sc, borderRight: cellBorder }}>
<ExecutionLog decisions={decisions} height={432} />
</div>
<div style={{ ...sc, borderRight: cellBorder }}>
<RiskRadar positions={positions} account={account} config={config} fullStats={fullStats} />
</div>
<div style={sc}>
{/* live open positions (the book right now) */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Positions</span>
<span className="tm-sc">Current positions · live</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>{positions?.length ?? 0} open</span>
</div>
{positions && positions.length > 0 ? (
<table className="tm-mono" style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
<thead>
<tr className="tm-sc" style={{ fontSize: 9 }}>
<td style={{ padding: '0 0 3px' }}>symbol</td>
<td style={{ padding: '0 0 3px' }}>side</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>lev</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>PnL</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>return%</td>
</tr>
</thead>
<tbody>
{positions.map((p, i) => {
const long = /long|buy/i.test(p.side)
const win = (p.unrealized_pnl ?? 0) >= 0
return (
<tr key={`${p.symbol}-${i}`} style={{ borderTop: '1px solid var(--tm-hair)' }}>
<td style={{ padding: '5px 0', fontWeight: 500 }}>{baseLabel(p.symbol)}</td>
<td style={{ padding: '5px 0' }} className={long ? 'tm-up' : 'tm-dn'}>{long ? 'long' : 'short'}</td>
<td style={{ padding: '5px 0', textAlign: 'right', color: 'var(--tm-muted)' }}>{p.leverage}×</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{fmtUsd(p.unrealized_pnl, true)}</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{(p.unrealized_pnl_pct ?? 0).toFixed(2)}%</td>
</tr>
)
})}
</tbody>
</table>
) : <div className="tm-sc" style={{ padding: '8px 0' }}>No open positions.</div>}
<div className="tm-rule" style={{ margin: '12px 0 10px' }} />
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Recent trades</span>
<span className="tm-sc">Recent closes · symbol/side/time/pnl</span>
</div>
{recentTrades.length > 0 ? (
<table className="tm-mono" style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
<tbody>
{recentTrades.map((p) => {
const win = p.realized_pnl >= 0
return (
<tr key={p.id} style={{ borderTop: '1px solid var(--tm-hair)' }}>
<td style={{ padding: '5px 0', fontWeight: 500 }}>{baseLabel(p.symbol)}</td>
<td style={{ padding: '5px 0' }} className={p.side === 'long' || p.side === 'LONG' ? 'tm-up' : 'tm-dn'}>{p.side.toLowerCase()}</td>
<td style={{ padding: '5px 0', color: 'var(--tm-muted)' }}>{fmtTime(p.exit_time)}</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{fmtUsd(p.realized_pnl, true)}</td>
</tr>
)
})}
</tbody>
</table>
) : <div className="tm-sc" style={{ padding: '8px 0' }}>No closed trades yet.</div>}
</div>
</div>
<div className="tm-rule" />
{/* equity curve */}
{equityPath && (
<>
<div style={{ ...sc, paddingBottom: 4, display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Equity curve</span>
<span className="tm-sc">Account equity curve · all {equity?.length ?? 0} pts</span>
</div>
<div style={{ padding: '0 14px 10px' }}>
<svg width="100%" height={88} viewBox="0 0 680 80" preserveAspectRatio="none" role="img" aria-label="Equity curve" style={{ display: 'block' }}>
<line x1="0" y1="79" x2="680" y2="79" stroke="var(--tm-hair)" vectorEffect="non-scaling-stroke" />
<path d={equityPath} fill="none" stroke="var(--tm-red)" strokeWidth={2} vectorEffect="non-scaling-stroke" />
</svg>
</div>
<div className="tm-rule" />
</>
)}
{/* market net inflow — real Vergex flow-markets via claw402 */}
<div style={sc}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 8 }}>
<span className="tm-px" style={{ fontSize: 12 }}>Market net inflow</span>
<span className="tm-sc">Market net inflow · {flow?.data?.window || '1h'} · Vergex</span>
<span className="tm-sc" style={{ marginLeft: 'auto' }}>{flowItems.length} markets</span>
</div>
<FlowMarkets items={flowItems} window={flow?.data?.window} />
</div>
<div className="tm-rule" />
{/* by-symbol stats | latest decision */}
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1.3fr)' }}>
<div style={{ ...sc, borderRight: cellBorder }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
<span className="tm-px" style={{ fontSize: 11 }}>By symbol</span>
<span className="tm-sc">By-symbol history · trades/win/pnl</span>
</div>
{symbolStats.length > 0 ? symbolStats.map((s) => (
<div key={s.symbol} style={{ marginBottom: 7 }}>
<div className="tm-mono" style={{ display: 'flex', fontSize: 11, marginBottom: 2 }}>
<span style={{ fontWeight: 500 }}>{baseLabel(s.symbol)}</span>
<span className="tm-sc" style={{ marginLeft: 8 }}>{s.total_trades} trades · {s.win_rate.toFixed(0)}% win</span>
<span className={s.total_pnl >= 0 ? 'tm-up' : 'tm-dn'} style={{ marginLeft: 'auto' }}>{fmtUsd(s.total_pnl, true)}</span>
</div>
<div style={{ height: 4, background: 'var(--tm-hair)' }}>
<div style={{ height: 4, width: `${(s.total_trades / maxSymTrades) * 100}%`, background: s.total_pnl >= 0 ? 'var(--tm-up)' : 'var(--tm-dn)' }} />
</div>
</div>
)) : <div className="tm-sc">No symbol history.</div>}
</div>
<div style={sc}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Latest decision</span>
<span className="tm-sc">AI rationale</span>
{latest && <span className="tm-sc" style={{ marginLeft: 'auto' }}>{fmtTime(latest.timestamp)}{latest.decisions?.[0]?.confidence != null ? ` · conf ${latest.decisions[0].confidence}` : ''}</span>}
</div>
{latest ? (
<>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 8 }}>
{(latest.decisions ?? []).slice(0, 5).map((d, i) => (
<span key={i} className="tm-mono" style={{ fontSize: 11, background: 'var(--tm-red-soft)', color: '#7a1f16', padding: '2px 9px' }}>{d.action} {baseLabel(d.symbol)}</span>
))}
</div>
<p style={{ fontSize: 12, lineHeight: 1.6, color: 'var(--tm-ink-2)', margin: '0 0 8px', borderLeft: '2px solid var(--tm-red)', paddingLeft: 12 }}>
{latest.decisions?.[0]?.reasoning || latest.cot_trace?.slice(0, 320) || 'No reasoning recorded.'}
</p>
{(latest.execution_log ?? []).length > 0 && (
<div className="tm-mono" style={{ fontSize: 10, color: 'var(--tm-muted)' }}>
{(latest.execution_log ?? []).slice(0, 3).map((l, i) => <div key={i}>{l}</div>)}
</div>
)}
</>
) : <div className="tm-sc">No decisions recorded yet.</div>}
</div>
</div>
</div>
</div>
)
}
export default TerminalDashboard

Some files were not shown because too many files have changed in this diff Show More