mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 04:20:59 +08:00
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:
@@ -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
58
api/handler_stats_full.go
Normal 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)
|
||||
}
|
||||
@@ -61,21 +61,21 @@ type UpdateTraderRequest struct {
|
||||
|
||||
func formatTraderCreationError(reason, nextStep string) string {
|
||||
if nextStep == "" {
|
||||
return fmt.Sprintf("这次未能创建机器人:%s。", reason)
|
||||
return fmt.Sprintf("Failed to create the bot this time: %s.", reason)
|
||||
}
|
||||
return fmt.Sprintf("这次未能创建机器人:%s。%s。", reason, nextStep)
|
||||
return fmt.Sprintf("Failed to create the bot this time: %s. %s.", reason, nextStep)
|
||||
}
|
||||
|
||||
func traderCreationRequestError(reason string) string {
|
||||
return formatTraderCreationError(reason, "请检查你刚刚填写的内容后,再重新提交")
|
||||
return formatTraderCreationError(reason, "Please check the information you just entered and submit again")
|
||||
}
|
||||
|
||||
func validateTraderLeverageRange(btcEthLeverage, altcoinLeverage int) (string, string) {
|
||||
if btcEthLeverage < 0 || btcEthLeverage > maxManualBTCETHLeverage {
|
||||
return traderCreationRequestError("BTC/ETH 杠杆倍数需要在 1 到 20 倍之间"), "trader.create.invalid_btc_eth_leverage"
|
||||
return traderCreationRequestError("BTC/ETH leverage must be between 1x and 20x"), "trader.create.invalid_btc_eth_leverage"
|
||||
}
|
||||
if altcoinLeverage < 0 || altcoinLeverage > maxManualAltLeverage {
|
||||
return traderCreationRequestError("山寨币杠杆倍数需要在 1 到 20 倍之间"), "trader.create.invalid_altcoin_leverage"
|
||||
return traderCreationRequestError("Altcoin leverage must be between 1x and 20x"), "trader.create.invalid_altcoin_leverage"
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
@@ -90,15 +90,15 @@ func isSupportedTraderSymbol(symbol string) bool {
|
||||
|
||||
func exchangeDisplayName(exchange *store.Exchange) string {
|
||||
if exchange == nil {
|
||||
return "所选交易所账户"
|
||||
return "the selected exchange account"
|
||||
}
|
||||
if exchange.AccountName != "" {
|
||||
return fmt.Sprintf("%s(%s)", exchange.Name, exchange.AccountName)
|
||||
return fmt.Sprintf("%s (%s)", exchange.Name, exchange.AccountName)
|
||||
}
|
||||
if exchange.Name != "" {
|
||||
return exchange.Name
|
||||
}
|
||||
return "所选交易所账户"
|
||||
return "the selected exchange account"
|
||||
}
|
||||
|
||||
func missingExchangeFields(exchange *store.Exchange) []string {
|
||||
@@ -127,10 +127,10 @@ func missingExchangeFields(exchange *store.Exchange) []string {
|
||||
}
|
||||
case "hyperliquid":
|
||||
if exchange.APIKey == "" {
|
||||
missing = append(missing, "私钥")
|
||||
missing = append(missing, "Private Key")
|
||||
}
|
||||
if strings.TrimSpace(exchange.HyperliquidWalletAddr) == "" {
|
||||
missing = append(missing, "钱包地址")
|
||||
missing = append(missing, "Wallet Address")
|
||||
}
|
||||
case "aster":
|
||||
if strings.TrimSpace(exchange.AsterUser) == "" {
|
||||
@@ -144,7 +144,7 @@ func missingExchangeFields(exchange *store.Exchange) []string {
|
||||
}
|
||||
case "lighter":
|
||||
if strings.TrimSpace(exchange.LighterWalletAddr) == "" {
|
||||
missing = append(missing, "钱包地址")
|
||||
missing = append(missing, "Wallet Address")
|
||||
}
|
||||
if exchange.LighterAPIKeyPrivateKey == "" {
|
||||
missing = append(missing, "API Key Private Key")
|
||||
@@ -168,21 +168,21 @@ func mapStringPairs(kv ...string) map[string]string {
|
||||
|
||||
func validateExchangeForTraderCreation(exchange *store.Exchange) (string, string, map[string]string) {
|
||||
if exchange == nil {
|
||||
return formatTraderCreationError("还没有找到你选择的交易所账户", "请前往「设置 > 交易所配置」先添加一个可用账户,再回来创建机器人"),
|
||||
return formatTraderCreationError("The exchange account you selected was not found", "Please go to \"Settings > Exchange Config\" and add an available account first, then come back to create the bot"),
|
||||
"trader.create.exchange_not_found", nil
|
||||
}
|
||||
if !exchange.Enabled {
|
||||
return formatTraderCreationError(
|
||||
fmt.Sprintf("交易所账户「%s」目前处于未启用状态", exchangeDisplayName(exchange)),
|
||||
"请前往「设置 > 交易所配置」启用该账户后,再重新创建机器人",
|
||||
fmt.Sprintf("Exchange account \"%s\" is currently disabled", exchangeDisplayName(exchange)),
|
||||
"Please go to \"Settings > Exchange Config\" to enable this account, then create the bot again",
|
||||
), "trader.create.exchange_disabled", mapStringPairs("exchange_name", exchangeDisplayName(exchange))
|
||||
}
|
||||
|
||||
missing := missingExchangeFields(exchange)
|
||||
if len(missing) > 0 {
|
||||
return formatTraderCreationError(
|
||||
fmt.Sprintf("交易所账户「%s」的配置还不完整,缺少 %s", exchangeDisplayName(exchange), strings.Join(missing, "、")),
|
||||
"请前往「设置 > 交易所配置」补全该账户的必填信息后,再重新创建机器人",
|
||||
fmt.Sprintf("The configuration for exchange account \"%s\" is incomplete, missing %s", exchangeDisplayName(exchange), strings.Join(missing, ", ")),
|
||||
"Please go to \"Settings > Exchange Config\" to complete the required information for this account, then create the bot again",
|
||||
), "trader.create.exchange_missing_fields", mapStringPairs(
|
||||
"exchange_name", exchangeDisplayName(exchange),
|
||||
"missing_fields", strings.Join(missing, ", "),
|
||||
@@ -194,8 +194,8 @@ func validateExchangeForTraderCreation(exchange *store.Exchange) (string, string
|
||||
return "", "", nil
|
||||
default:
|
||||
return formatTraderCreationError(
|
||||
fmt.Sprintf("交易所账户「%s」使用了当前版本暂不支持的类型 %s", exchangeDisplayName(exchange), exchange.ExchangeType),
|
||||
"请改用当前版本支持的交易所账户后,再重新创建机器人",
|
||||
fmt.Sprintf("Exchange account \"%s\" uses type %s, which is not supported in the current version", exchangeDisplayName(exchange), exchange.ExchangeType),
|
||||
"Please switch to an exchange account supported by the current version, then create the bot again",
|
||||
), "trader.create.exchange_unsupported", mapStringPairs(
|
||||
"exchange_name", exchangeDisplayName(exchange),
|
||||
"exchange_type", exchange.ExchangeType,
|
||||
@@ -214,28 +214,28 @@ func classifyTraderSetupReason(reason string) (string, string) {
|
||||
switch {
|
||||
case strings.Contains(lower, "failed to parse strategy config"),
|
||||
strings.Contains(lower, "failed to parse strategy configuration"):
|
||||
return "trader.reason.strategy_config_invalid", "当前策略配置内容已损坏,系统暂时无法解析"
|
||||
return "trader.reason.strategy_config_invalid", "The current strategy configuration is corrupted and the system cannot parse it for now"
|
||||
case strings.Contains(lower, "has no strategy configured"):
|
||||
return "trader.reason.strategy_missing", "当前机器人缺少有效的交易策略配置"
|
||||
return "trader.reason.strategy_missing", "The current bot is missing a valid trading strategy configuration"
|
||||
case strings.Contains(lower, "failed to parse private key"),
|
||||
(strings.Contains(lower, "invalid hex character") && strings.Contains(lower, "private key")):
|
||||
return "trader.reason.private_key_invalid", "私钥格式不正确,系统无法识别"
|
||||
return "trader.reason.private_key_invalid", "The private key format is incorrect and the system cannot recognize it"
|
||||
case strings.Contains(lower, "failed to initialize hyperliquid trader"):
|
||||
return "trader.reason.hyperliquid_init_failed", "Hyperliquid 账户初始化失败,请确认私钥、主钱包地址和 Agent Wallet 配置是否正确"
|
||||
return "trader.reason.hyperliquid_init_failed", "Hyperliquid account initialization failed; please confirm the private key, main wallet address, and Agent Wallet configuration are correct"
|
||||
case strings.Contains(lower, "failed to initialize aster trader"):
|
||||
return "trader.reason.aster_init_failed", "Aster 账户初始化失败,请确认 Aster User、Signer 和私钥是否正确"
|
||||
return "trader.reason.aster_init_failed", "Aster account initialization failed; please confirm the Aster User, Signer, and private key are correct"
|
||||
case strings.Contains(lower, "failed to get meta information"):
|
||||
return "trader.reason.exchange_meta_unavailable", "系统暂时无法从交易所读取账户元信息"
|
||||
return "trader.reason.exchange_meta_unavailable", "The system cannot read account meta information from the exchange for now"
|
||||
case strings.Contains(lower, "security check failed") && strings.Contains(lower, "agent wallet balance too high"):
|
||||
return "trader.reason.hyperliquid_agent_balance_too_high", "Hyperliquid Agent Wallet 余额过高,不符合当前安全要求"
|
||||
return "trader.reason.hyperliquid_agent_balance_too_high", "The Hyperliquid Agent Wallet balance is too high and does not meet the current security requirements"
|
||||
case strings.Contains(lower, "failed to initialize account"):
|
||||
return "trader.reason.exchange_account_init_failed", "交易所账户初始化失败,请确认钱包地址和 API Key 是否匹配"
|
||||
return "trader.reason.exchange_account_init_failed", "Exchange account initialization failed; please confirm the wallet address and API Key match"
|
||||
case strings.Contains(lower, "unsupported trading platform"):
|
||||
return "trader.reason.exchange_unsupported", "当前交易所类型暂不支持机器人初始化"
|
||||
return "trader.reason.exchange_unsupported", "The current exchange type does not support bot initialization"
|
||||
case strings.Contains(lower, "initial balance not set and unable to fetch balance from exchange"):
|
||||
return "trader.reason.exchange_balance_unavailable", "系统暂时无法从交易所读取账户余额"
|
||||
return "trader.reason.exchange_balance_unavailable", "The system cannot read the account balance from the exchange for now"
|
||||
case strings.Contains(lower, "timeout"), strings.Contains(lower, "no such host"), strings.Contains(lower, "connection refused"):
|
||||
return "trader.reason.exchange_service_unreachable", "系统暂时无法连接交易所服务"
|
||||
return "trader.reason.exchange_service_unreachable", "The system cannot connect to the exchange service for now"
|
||||
default:
|
||||
return "trader.reason.unknown", trimmed
|
||||
}
|
||||
@@ -270,54 +270,54 @@ func traderSetupReasonParams(err error, fallback string, kv ...string) map[strin
|
||||
|
||||
func describeTraderLoadError(traderName string, err error) string {
|
||||
if err == nil {
|
||||
return formatTraderCreationError("机器人配置虽然保存了,但运行实例没有成功初始化", "请检查模型、策略和交易所配置是否完整,然后再试一次")
|
||||
return formatTraderCreationError("The bot configuration was saved, but the runtime instance failed to initialize", "Please check that the model, strategy, and exchange configuration are complete, then try again")
|
||||
}
|
||||
|
||||
reason := humanizeTraderSetupReason(SanitizeError(err, ""))
|
||||
if reason == "" {
|
||||
return formatTraderCreationError(
|
||||
fmt.Sprintf("机器人「%s」在初始化运行实例时没有成功启动", traderName),
|
||||
"请检查模型、策略和交易所配置是否完整,然后再试一次",
|
||||
fmt.Sprintf("Bot \"%s\" failed to start when initializing its runtime instance", traderName),
|
||||
"Please check that the model, strategy, and exchange configuration are complete, then try again",
|
||||
)
|
||||
}
|
||||
|
||||
return formatTraderCreationError(
|
||||
fmt.Sprintf("机器人「%s」在初始化运行实例时没有成功启动,原因是:%s", traderName, reason),
|
||||
"请检查模型、策略和交易所配置是否完整,然后再试一次",
|
||||
fmt.Sprintf("Bot \"%s\" failed to start when initializing its runtime instance, because: %s", traderName, reason),
|
||||
"Please check that the model, strategy, and exchange configuration are complete, then try again",
|
||||
)
|
||||
}
|
||||
|
||||
func describeTraderCreationWarning(traderName string, err error) string {
|
||||
if err == nil {
|
||||
return fmt.Sprintf("机器人「%s」已经保存,但当前还没有通过启动前校验。请先检查模型、策略和交易所配置,修正后再点击启动。", traderName)
|
||||
return fmt.Sprintf("Bot \"%s\" has been saved, but it has not yet passed the pre-start validation. Please check the model, strategy, and exchange configuration first, then click start after fixing them.", traderName)
|
||||
}
|
||||
|
||||
reason := humanizeTraderSetupReason(SanitizeError(err, ""))
|
||||
if reason == "" {
|
||||
return fmt.Sprintf("机器人「%s」已经保存,但当前暂时还不能启动。请先检查模型、策略和交易所配置,修正后再点击启动。", traderName)
|
||||
return fmt.Sprintf("Bot \"%s\" has been saved, but it cannot start for now. Please check the model, strategy, and exchange configuration first, then click start after fixing them.", traderName)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("机器人「%s」已经保存,但当前暂时还不能启动,原因是:%s。请先检查模型、策略和交易所配置,修正后再点击启动。", traderName, reason)
|
||||
return fmt.Sprintf("Bot \"%s\" has been saved, but it cannot start for now, because: %s. Please check the model, strategy, and exchange configuration first, then click start after fixing them.", traderName, reason)
|
||||
}
|
||||
|
||||
func describeTraderStartError(traderName string, err error) string {
|
||||
if err == nil {
|
||||
return fmt.Sprintf("这次未能启动机器人:机器人「%s」暂时还不能启动。请检查模型、策略和交易所配置后,再重新点击启动。", traderName)
|
||||
return fmt.Sprintf("Failed to start the bot this time: bot \"%s\" cannot start for now. Please check the model, strategy, and exchange configuration, then click start again.", traderName)
|
||||
}
|
||||
|
||||
reason := humanizeTraderSetupReason(SanitizeError(err, ""))
|
||||
if reason == "" {
|
||||
return fmt.Sprintf("这次未能启动机器人:机器人「%s」暂时还不能启动。请检查模型、策略和交易所配置后,再重新点击启动。", traderName)
|
||||
return fmt.Sprintf("Failed to start the bot this time: bot \"%s\" cannot start for now. Please check the model, strategy, and exchange configuration, then click start again.", traderName)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("这次未能启动机器人:机器人「%s」暂时还不能启动,原因是:%s。请检查模型、策略和交易所配置后,再重新点击启动。", traderName, reason)
|
||||
return fmt.Sprintf("Failed to start the bot this time: bot \"%s\" cannot start for now, because: %s. Please check the model, strategy, and exchange configuration, then click start again.", traderName, reason)
|
||||
}
|
||||
|
||||
func formatTraderStartError(reason, nextStep string) string {
|
||||
if nextStep == "" {
|
||||
return fmt.Sprintf("这次未能启动机器人:%s。", reason)
|
||||
return fmt.Sprintf("Failed to start the bot this time: %s.", reason)
|
||||
}
|
||||
return fmt.Sprintf("这次未能启动机器人:%s。%s。", reason, nextStep)
|
||||
return fmt.Sprintf("Failed to start the bot this time: %s. %s.", reason, nextStep)
|
||||
}
|
||||
|
||||
// handleCreateTrader Create new AI trader
|
||||
@@ -325,7 +325,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
var req CreateTraderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
SafeBadRequestWithDetails(c, traderCreationRequestError("提交的信息不完整,或者格式不正确"), "trader.create.invalid_request", nil)
|
||||
SafeBadRequestWithDetails(c, traderCreationRequestError("The submitted information is incomplete or has an invalid format"), "trader.create.invalid_request", nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
symbol = strings.TrimSpace(symbol)
|
||||
if !isSupportedTraderSymbol(symbol) {
|
||||
SafeBadRequestWithDetails(c, traderCreationRequestError(
|
||||
fmt.Sprintf("交易对 %s 的格式不正确,目前只支持 USDT 合约或 Hyperliquid XYZ USDC 标的(SYMBOL-USDC)", symbol),
|
||||
fmt.Sprintf("The trading pair %s has an invalid format; only USDT perpetuals or Hyperliquid XYZ USDC instruments (SYMBOL-USDC) are currently supported", symbol),
|
||||
), "trader.create.invalid_symbol", mapStringPairs("symbol", symbol))
|
||||
return
|
||||
}
|
||||
@@ -354,32 +354,32 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
model, err := s.store.AIModel().Get(userID, req.AIModelID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError("还没有找到你选择的 AI 模型", "请前往「设置 > 模型配置」先添加并启用一个可用模型,再回来创建机器人"), "trader.create.model_not_found", nil)
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError("The AI model you selected was not found", "Please go to \"Settings > Model Config\" to add and enable an available model first, then come back to create the bot"), "trader.create.model_not_found", nil)
|
||||
return
|
||||
}
|
||||
SafeError(c, http.StatusInternalServerError,
|
||||
formatTraderCreationError("暂时无法读取你的 AI 模型配置", "请稍后重试;如果问题持续,再检查本地服务是否正常"),
|
||||
formatTraderCreationError("Unable to read your AI model configuration for now", "Please retry later; if the problem persists, check whether the local service is running normally"),
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
if !model.Enabled {
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError(
|
||||
fmt.Sprintf("AI 模型「%s」目前还没有启用", model.Name),
|
||||
"请前往「设置 > 模型配置」启用它后,再重新创建机器人",
|
||||
fmt.Sprintf("AI model \"%s\" is not enabled yet", model.Name),
|
||||
"Please go to \"Settings > Model Config\" to enable it, then create the bot again",
|
||||
), "trader.create.model_disabled", mapStringPairs("model_name", model.Name))
|
||||
return
|
||||
}
|
||||
if model.APIKey == "" {
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError(
|
||||
fmt.Sprintf("AI 模型「%s」缺少 API Key 或支付凭证", model.Name),
|
||||
"请前往「设置 > 模型配置」补全模型凭证后,再重新创建机器人",
|
||||
fmt.Sprintf("AI model \"%s\" is missing an API Key or payment credentials", model.Name),
|
||||
"Please go to \"Settings > Model Config\" to complete the model credentials, then create the bot again",
|
||||
), "trader.create.model_missing_credentials", mapStringPairs("model_name", model.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if req.StrategyID == "" {
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError("你还没有选择交易策略", "请先选择一个策略,再继续创建机器人"), "trader.create.strategy_required", nil)
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError("You have not selected a trading strategy yet", "Please select a strategy first, then continue creating the bot"), "trader.create.strategy_required", nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -387,11 +387,11 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
_, err = s.store.Strategy().Get(userID, req.StrategyID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError("你选择的策略不存在,或者已经被删除了", "请重新选择一个可用策略后,再继续创建机器人"), "trader.create.strategy_not_found", nil)
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError("The strategy you selected does not exist or has been deleted", "Please select another available strategy, then continue creating the bot"), "trader.create.strategy_not_found", nil)
|
||||
return
|
||||
}
|
||||
SafeError(c, http.StatusInternalServerError,
|
||||
formatTraderCreationError("暂时无法读取你选择的策略配置", "请稍后重试;如果问题持续,再检查本地服务是否正常"),
|
||||
formatTraderCreationError("Unable to read the strategy configuration you selected for now", "Please retry later; if the problem persists, check whether the local service is running normally"),
|
||||
err,
|
||||
)
|
||||
return
|
||||
@@ -445,7 +445,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
exchanges, err := s.store.Exchange().List(userID)
|
||||
if err != nil {
|
||||
SafeError(c, http.StatusInternalServerError,
|
||||
formatTraderCreationError("暂时无法读取你的交易所配置", "请稍后重试;如果问题持续,再检查本地服务是否正常"),
|
||||
formatTraderCreationError("Unable to read your exchange configuration for now", "Please retry later; if the problem persists, check whether the local service is running normally"),
|
||||
err,
|
||||
)
|
||||
return
|
||||
@@ -469,9 +469,9 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
tempTrader, createErr := buildExchangeProbeTrader(exchangeCfg, userID)
|
||||
if createErr != nil {
|
||||
SafeBadRequestWithDetails(c, formatTraderCreationError(
|
||||
fmt.Sprintf("交易所账户「%s」没有通过初始化校验,原因是:%s", exchangeDisplayName(exchangeCfg), humanizeTraderSetupReason(SanitizeError(createErr, "配置校验未通过"))),
|
||||
"请前往「设置 > 交易所配置」检查这个账户的密钥、地址和账户信息是否填写正确",
|
||||
), "trader.create.exchange_probe_failed", traderSetupReasonParams(createErr, "配置校验未通过",
|
||||
fmt.Sprintf("Exchange account \"%s\" did not pass initialization validation, because: %s", exchangeDisplayName(exchangeCfg), humanizeTraderSetupReason(SanitizeError(createErr, "Configuration validation failed"))),
|
||||
"Please go to \"Settings > Exchange Config\" to check whether this account's keys, address, and account information are entered correctly",
|
||||
), "trader.create.exchange_probe_failed", traderSetupReasonParams(createErr, "Configuration validation failed",
|
||||
"exchange_name", exchangeDisplayName(exchangeCfg),
|
||||
))
|
||||
return
|
||||
@@ -521,9 +521,9 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
err = s.store.Trader().Create(traderRecord)
|
||||
if err != nil {
|
||||
logger.Infof("❌ Failed to create trader: %v", err)
|
||||
publicMsg := SanitizeError(err, formatTraderCreationError("机器人配置没有保存成功", "请检查名称、模型、策略和交易所配置后,再试一次"))
|
||||
publicMsg := SanitizeError(err, formatTraderCreationError("The bot configuration was not saved successfully", "Please check the name, model, strategy, and exchange configuration, then try again"))
|
||||
statusCode := http.StatusBadRequest
|
||||
if publicMsg == formatTraderCreationError("机器人配置没有保存成功", "请检查名称、模型、策略和交易所配置后,再试一次") {
|
||||
if publicMsg == formatTraderCreationError("The bot configuration was not saved successfully", "Please check the name, model, strategy, and exchange configuration, then try again") {
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
SafeError(c, statusCode, publicMsg, err)
|
||||
@@ -781,8 +781,8 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
||||
|
||||
if fullCfg != nil && fullCfg.Exchange != nil && fullCfg.Exchange.ExchangeType == "hyperliquid" && !fullCfg.Exchange.HyperliquidBuilderApproved {
|
||||
SafeBadRequestWithDetails(c, formatTraderStartError(
|
||||
fmt.Sprintf("机器人「%s」的 Hyperliquid 交易授权尚未完成", traderName),
|
||||
"请重新连接 Hyperliquid 钱包并完成交易授权后,再启动机器人",
|
||||
fmt.Sprintf("The Hyperliquid trading authorization for bot \"%s\" is not yet complete", traderName),
|
||||
"Please reconnect the Hyperliquid wallet and complete the trading authorization, then start the bot",
|
||||
), "trader.start.hyperliquid_builder_not_approved", mapStringPairs("trader_name", traderName, "exchange_name", exchangeDisplayName(fullCfg.Exchange)))
|
||||
return
|
||||
}
|
||||
@@ -818,25 +818,25 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
||||
}
|
||||
// Check AI model
|
||||
if fullCfg.AIModel == nil {
|
||||
SafeBadRequestWithDetails(c, formatTraderStartError("这个机器人关联的 AI 模型不存在", "请前往「设置 > 模型配置」检查后,再重新点击启动"), "trader.start.model_not_found", mapStringPairs("trader_name", traderName))
|
||||
SafeBadRequestWithDetails(c, formatTraderStartError("The AI model associated with this bot does not exist", "Please go to \"Settings > Model Config\" to check, then click start again"), "trader.start.model_not_found", mapStringPairs("trader_name", traderName))
|
||||
return
|
||||
}
|
||||
if !fullCfg.AIModel.Enabled {
|
||||
SafeBadRequestWithDetails(c, formatTraderStartError(
|
||||
fmt.Sprintf("机器人「%s」关联的 AI 模型「%s」目前还没有启用", traderName, fullCfg.AIModel.Name),
|
||||
"请前往「设置 > 模型配置」启用它后,再重新点击启动",
|
||||
fmt.Sprintf("The AI model \"%s\" associated with bot \"%s\" is not enabled yet", fullCfg.AIModel.Name, traderName),
|
||||
"Please go to \"Settings > Model Config\" to enable it, then click start again",
|
||||
), "trader.start.model_disabled", mapStringPairs("trader_name", traderName, "model_name", fullCfg.AIModel.Name))
|
||||
return
|
||||
}
|
||||
// Check exchange
|
||||
if fullCfg.Exchange == nil {
|
||||
SafeBadRequestWithDetails(c, formatTraderStartError("这个机器人关联的交易所账户不存在", "请前往「设置 > 交易所配置」检查后,再重新点击启动"), "trader.start.exchange_not_found", mapStringPairs("trader_name", traderName))
|
||||
SafeBadRequestWithDetails(c, formatTraderStartError("The exchange account associated with this bot does not exist", "Please go to \"Settings > Exchange Config\" to check, then click start again"), "trader.start.exchange_not_found", mapStringPairs("trader_name", traderName))
|
||||
return
|
||||
}
|
||||
if !fullCfg.Exchange.Enabled {
|
||||
SafeBadRequestWithDetails(c, formatTraderStartError(
|
||||
fmt.Sprintf("机器人「%s」关联的交易所账户「%s」目前还没有启用", traderName, exchangeDisplayName(fullCfg.Exchange)),
|
||||
"请前往「设置 > 交易所配置」启用它后,再重新点击启动",
|
||||
fmt.Sprintf("The exchange account \"%s\" associated with bot \"%s\" is not enabled yet", exchangeDisplayName(fullCfg.Exchange), traderName),
|
||||
"Please go to \"Settings > Exchange Config\" to enable it, then click start again",
|
||||
), "trader.start.exchange_disabled", mapStringPairs("trader_name", traderName, "exchange_name", exchangeDisplayName(fullCfg.Exchange)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
36
kernel/engine_directional_test.go
Normal file
36
kernel/engine_directional_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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"),
|
||||
|
||||
190
kernel/schema.go
190
kernel/schema.go
@@ -62,156 +62,156 @@ func (d BilingualFieldDef) GetDesc(lang Language) string {
|
||||
var DataDictionary = map[string]map[string]BilingualFieldDef{
|
||||
"AccountMetrics": {
|
||||
"Equity": {
|
||||
NameZH: "总权益",
|
||||
NameZH: "Total Equity",
|
||||
NameEN: "Total Equity",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "可用余额 + 未实现盈亏",
|
||||
FormulaZH: "Available Balance + Unrealized PnL",
|
||||
FormulaEN: "Available Balance + Unrealized PnL",
|
||||
DescZH: "账户的实际净值,包含所有持仓的浮动盈亏",
|
||||
DescZH: "Actual net value of the account, including unrealized P&L of all positions",
|
||||
DescEN: "Actual account value including all unrealized P&L from positions",
|
||||
},
|
||||
"Balance": {
|
||||
NameZH: "可用余额",
|
||||
NameZH: "Available Balance",
|
||||
NameEN: "Available Balance",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "初始资金 + 已实现盈亏",
|
||||
FormulaZH: "Initial Capital + Realized PnL",
|
||||
FormulaEN: "Initial Capital + Realized PnL",
|
||||
DescZH: "可用于开新仓位的资金,不包括已用保证金",
|
||||
DescZH: "Funds available for opening new positions, excluding used margin",
|
||||
DescEN: "Available funds for opening new positions, excluding used margin",
|
||||
},
|
||||
"PnL": {
|
||||
NameZH: "总盈亏百分比",
|
||||
NameZH: "Total PnL Percentage",
|
||||
NameEN: "Total PnL Percentage",
|
||||
Unit: "%",
|
||||
FormulaZH: "(总权益 - 初始资金) / 初始资金 × 100",
|
||||
FormulaZH: "(Total Equity - Initial Capital) / Initial Capital × 100",
|
||||
FormulaEN: "(Total Equity - Initial Capital) / Initial Capital × 100",
|
||||
DescZH: "自系统启动以来的总收益率,+15.87%表示盈利15.87%",
|
||||
DescZH: "Total return since system startup, +15.87% means 15.87% profit",
|
||||
DescEN: "Total return since inception, +15.87% means 15.87% profit",
|
||||
},
|
||||
"Margin": {
|
||||
NameZH: "保证金使用率",
|
||||
NameZH: "Margin Usage Rate",
|
||||
NameEN: "Margin Usage Rate",
|
||||
Unit: "%",
|
||||
FormulaZH: "已用保证金合计 / 总权益 × 100",
|
||||
FormulaZH: "Total Used Margin / Total Equity × 100",
|
||||
FormulaEN: "Total Used Margin / Total Equity × 100",
|
||||
DescZH: "该值越高,账户风险越大。安全值<30%,危险值>70%",
|
||||
DescZH: "The higher this value, the greater the account risk. Safe <30%, Dangerous >70%",
|
||||
DescEN: "Higher value = higher risk. Safe <30%, Dangerous >70%",
|
||||
},
|
||||
},
|
||||
|
||||
"TradeMetrics": {
|
||||
"Entry": {
|
||||
NameZH: "进场价",
|
||||
NameZH: "Entry Price",
|
||||
NameEN: "Entry Price",
|
||||
Unit: "USDT",
|
||||
DescZH: "开仓时的平均价格",
|
||||
DescZH: "Average price when opening the position",
|
||||
DescEN: "Average price when opening position",
|
||||
},
|
||||
"Exit": {
|
||||
NameZH: "出场价",
|
||||
NameZH: "Exit Price",
|
||||
NameEN: "Exit Price",
|
||||
Unit: "USDT",
|
||||
DescZH: "平仓时的平均价格",
|
||||
DescZH: "Average price when closing the position",
|
||||
DescEN: "Average price when closing position",
|
||||
},
|
||||
"Profit": {
|
||||
NameZH: "已实现盈亏",
|
||||
NameZH: "Realized PnL",
|
||||
NameEN: "Realized PnL",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "(出场价 - 进场价) / 进场价 × 杠杆 × 仓位价值",
|
||||
FormulaZH: "(Exit Price - Entry Price) / Entry Price × Leverage × Position Value",
|
||||
FormulaEN: "(Exit Price - Entry Price) / Entry Price × Leverage × Position Value",
|
||||
DescZH: "已平仓交易的实际盈亏,包含手续费。正值=盈利,负值=亏损",
|
||||
DescZH: "Actual P&L of closed trades, including fees. Positive=profit, Negative=loss",
|
||||
DescEN: "Actual profit/loss of closed trades including fees. Positive=profit, Negative=loss",
|
||||
},
|
||||
"PnL%": {
|
||||
NameZH: "盈亏百分比",
|
||||
NameZH: "PnL Percentage",
|
||||
NameEN: "PnL Percentage",
|
||||
Unit: "%",
|
||||
FormulaZH: "(出场价 - 进场价) / 进场价 × 杠杆 × 100",
|
||||
FormulaZH: "(Exit - Entry) / Entry × Leverage × 100",
|
||||
FormulaEN: "(Exit - Entry) / Entry × Leverage × 100",
|
||||
DescZH: "已平仓交易的收益率,+6.71%表示盈利6.71%",
|
||||
DescZH: "Return on a closed trade, +6.71% means 6.71% profit",
|
||||
DescEN: "Return on closed trade, +6.71% means 6.71% profit",
|
||||
},
|
||||
"HoldDuration": {
|
||||
NameZH: "持仓时长",
|
||||
NameZH: "Holding Duration",
|
||||
NameEN: "Holding Duration",
|
||||
Unit: "minutes",
|
||||
DescZH: "从开仓到平仓的时间。<15分钟=超短线,15分钟-4小时=日内,>4小时=波段",
|
||||
DescZH: "Time from open to close. <15min=scalping, 15min-4h=intraday, >4h=swing",
|
||||
DescEN: "Time from open to close. <15min=scalping, 15min-4h=intraday, >4h=swing",
|
||||
},
|
||||
},
|
||||
|
||||
"PositionMetrics": {
|
||||
"UnrealizedPnL%": {
|
||||
NameZH: "未实现盈亏百分比",
|
||||
NameZH: "Unrealized PnL Percentage",
|
||||
NameEN: "Unrealized PnL Percentage",
|
||||
Unit: "%",
|
||||
FormulaZH: "(当前价 - 进场价) / 进场价 × 杠杆 × 100",
|
||||
FormulaZH: "(Current Price - Entry Price) / Entry Price × Leverage × 100",
|
||||
FormulaEN: "(Current Price - Entry Price) / Entry Price × Leverage × 100",
|
||||
DescZH: "当前持仓的浮动盈亏,未平仓前是浮动的",
|
||||
DescZH: "Floating P&L of the current position, fluctuating until closed",
|
||||
DescEN: "Floating P&L of current position, not realized until closed",
|
||||
},
|
||||
"PeakPnL%": {
|
||||
NameZH: "峰值盈亏百分比",
|
||||
NameZH: "Peak PnL Percentage",
|
||||
NameEN: "Peak PnL Percentage",
|
||||
Unit: "%",
|
||||
DescZH: "该持仓曾经达到的最高未实现盈亏。用于判断是否需要止盈",
|
||||
DescZH: "Highest unrealized P&L this position has reached. Used to decide whether to take profit",
|
||||
DescEN: "Historical max unrealized PnL for this position. Used for take-profit decisions",
|
||||
},
|
||||
"Drawdown": {
|
||||
NameZH: "从峰值回撤",
|
||||
NameZH: "Drawdown from Peak",
|
||||
NameEN: "Drawdown from Peak",
|
||||
Unit: "%",
|
||||
FormulaZH: "当前盈亏% - 峰值盈亏%",
|
||||
FormulaZH: "Current PnL% - Peak PnL%",
|
||||
FormulaEN: "Current PnL% - Peak PnL%",
|
||||
DescZH: "负值表示正在回撤。例如:峰值+5%,当前+3%,回撤=-2%",
|
||||
DescZH: "Negative value means pulling back. E.g., Peak +5%, Current +3%, Drawdown = -2%",
|
||||
DescEN: "Negative = pulling back. E.g., Peak +5%, Current +3%, Drawdown = -2%",
|
||||
},
|
||||
"Leverage": {
|
||||
NameZH: "杠杆倍数",
|
||||
NameZH: "Leverage",
|
||||
NameEN: "Leverage",
|
||||
Unit: "x",
|
||||
DescZH: "3x表示价格变动1%,持仓盈亏变动3%。杠杆越高,风险越大",
|
||||
DescZH: "3x means a 1% price move = 3% position PnL. Higher leverage = higher risk",
|
||||
DescEN: "3x means 1% price move = 3% position PnL. Higher leverage = higher risk",
|
||||
},
|
||||
"Margin": {
|
||||
NameZH: "占用保证金",
|
||||
NameZH: "Margin Used",
|
||||
NameEN: "Margin Used",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "仓位价值 / 杠杆",
|
||||
FormulaZH: "Position Value / Leverage",
|
||||
FormulaEN: "Position Value / Leverage",
|
||||
DescZH: "该仓位锁定的保证金金额",
|
||||
DescZH: "Amount of margin locked for this position",
|
||||
DescEN: "Collateral locked for this position",
|
||||
},
|
||||
"LiqPrice": {
|
||||
NameZH: "强平价格",
|
||||
NameZH: "Liquidation Price",
|
||||
NameEN: "Liquidation Price",
|
||||
Unit: "USDT",
|
||||
DescZH: "价格触及此值时会被强制平仓。0.0000表示无爆仓风险",
|
||||
DescZH: "Position will be force-closed when price reaches this value. 0.0000 = no liquidation risk",
|
||||
DescEN: "Price at which position will be force-closed. 0.0000 = no liquidation risk",
|
||||
},
|
||||
},
|
||||
|
||||
"MarketData": {
|
||||
"Volume": {
|
||||
NameZH: "成交量",
|
||||
NameZH: "Volume",
|
||||
NameEN: "Volume",
|
||||
Unit: "base asset",
|
||||
DescZH: "该时间段的交易量",
|
||||
DescZH: "Trading volume in this period",
|
||||
DescEN: "Trading volume in this period",
|
||||
},
|
||||
"OI": {
|
||||
NameZH: "持仓量",
|
||||
NameZH: "Open Interest",
|
||||
NameEN: "Open Interest",
|
||||
Unit: "USDT",
|
||||
DescZH: "未平仓合约的总价值。持仓量增加=资金流入,减少=资金流出",
|
||||
DescZH: "Total value of open contracts. Increasing OI = capital inflow, decreasing = outflow",
|
||||
DescEN: "Total value of open contracts. Increasing OI = capital inflow, decreasing = outflow",
|
||||
},
|
||||
"OIChange": {
|
||||
NameZH: "持仓量变化",
|
||||
NameZH: "OI Change",
|
||||
NameEN: "OI Change",
|
||||
Unit: "USDT & %",
|
||||
DescZH: "1小时内持仓量的变化。用于判断市场真实资金流向",
|
||||
DescZH: "OI change within 1 hour. Used to judge the real market capital flow direction",
|
||||
DescEN: "OI change in 1 hour. Used to determine real capital flow direction",
|
||||
},
|
||||
},
|
||||
@@ -256,30 +256,30 @@ var TradingRules = struct {
|
||||
RiskManagement: map[string]BilingualRuleDef{
|
||||
"MaxMarginUsage": {
|
||||
Value: 0.30,
|
||||
DescZH: "保证金使用率不得超过30%",
|
||||
DescZH: "Margin usage must not exceed 30%",
|
||||
DescEN: "Margin usage must not exceed 30%",
|
||||
ReasonZH: "保留70%的资金应对极端行情和追加保证金",
|
||||
ReasonZH: "Reserve 70% capital for extreme market conditions and margin calls",
|
||||
ReasonEN: "Reserve 70% capital for extreme market conditions and margin calls",
|
||||
},
|
||||
"MaxPositionLoss": {
|
||||
Value: -0.05,
|
||||
DescZH: "单个持仓亏损达到-5%时必须止损",
|
||||
DescZH: "Must stop-loss when single position loss reaches -5%",
|
||||
DescEN: "Must stop-loss when single position loss reaches -5%",
|
||||
ReasonZH: "避免单笔交易造成过大损失",
|
||||
ReasonZH: "Prevent excessive loss from single trade",
|
||||
ReasonEN: "Prevent excessive loss from single trade",
|
||||
},
|
||||
"MaxDailyLoss": {
|
||||
Value: -0.10,
|
||||
DescZH: "单日亏损达到-10%时停止交易",
|
||||
DescZH: "Stop trading when daily loss reaches -10%",
|
||||
DescEN: "Stop trading when daily loss reaches -10%",
|
||||
ReasonZH: "防止情绪化交易导致连续亏损",
|
||||
ReasonZH: "Prevent emotional trading leading to consecutive losses",
|
||||
ReasonEN: "Prevent emotional trading leading to consecutive losses",
|
||||
},
|
||||
"PositionSizeLimit": {
|
||||
Value: 0.15,
|
||||
DescZH: "单个仓位不得超过总权益的15%",
|
||||
DescZH: "Single position must not exceed 15% of total equity",
|
||||
DescEN: "Single position must not exceed 15% of total equity",
|
||||
ReasonZH: "避免过度集中风险",
|
||||
ReasonZH: "Avoid excessive risk concentration",
|
||||
ReasonEN: "Avoid excessive risk concentration",
|
||||
},
|
||||
},
|
||||
@@ -287,16 +287,16 @@ var TradingRules = struct {
|
||||
EntrySignals: map[string]BilingualRuleDef{
|
||||
"VolumeSpike": {
|
||||
Value: 2.0,
|
||||
DescZH: "成交量是平均值的2倍以上时考虑进场",
|
||||
DescZH: "Consider entry when volume is 2x above average",
|
||||
DescEN: "Consider entry when volume is 2x above average",
|
||||
ReasonZH: "放量突破通常意味着强趋势",
|
||||
ReasonZH: "Volume breakout usually indicates strong trend",
|
||||
ReasonEN: "Volume breakout usually indicates strong trend",
|
||||
},
|
||||
"OIChangeThreshold": {
|
||||
Value: 0.02,
|
||||
DescZH: "持仓量1小时内变化超过2%视为显著变化",
|
||||
DescZH: "OI change >2% in 1 hour is considered significant",
|
||||
DescEN: "OI change >2% in 1 hour is considered significant",
|
||||
ReasonZH: "大额资金进出会导致持仓量显著变化",
|
||||
ReasonZH: "Large capital flows cause significant OI changes",
|
||||
ReasonEN: "Large capital flows cause significant OI changes",
|
||||
},
|
||||
},
|
||||
@@ -304,16 +304,16 @@ var TradingRules = struct {
|
||||
ExitSignals: map[string]BilingualRuleDef{
|
||||
"TrailingStop": {
|
||||
Value: 0.30,
|
||||
DescZH: "当盈亏从峰值回撤30%时平仓止盈",
|
||||
DescZH: "Close position when PnL pulls back 30% from peak",
|
||||
DescEN: "Close position when PnL pulls back 30% from peak",
|
||||
ReasonZH: "锁定大部分利润,避免盈利回吐。例如:峰值+5%,回撤到+3.5%时平仓",
|
||||
ReasonZH: "Lock in most profits, avoid profit giveback. E.g., Peak +5%, close at +3.5%",
|
||||
ReasonEN: "Lock in most profits, avoid profit giveback. E.g., Peak +5%, close at +3.5%",
|
||||
},
|
||||
"StopLoss": {
|
||||
Value: -0.05,
|
||||
DescZH: "硬止损设置在-5%",
|
||||
DescZH: "Hard stop-loss at -5%",
|
||||
DescEN: "Hard stop-loss at -5%",
|
||||
ReasonZH: "严格控制单笔最大损失",
|
||||
ReasonZH: "Strictly control maximum single-trade loss",
|
||||
ReasonEN: "Strictly control maximum single-trade loss",
|
||||
},
|
||||
},
|
||||
@@ -321,9 +321,9 @@ var TradingRules = struct {
|
||||
PositionControl: map[string]BilingualRuleDef{
|
||||
"ScaleIn": {
|
||||
Value: map[string]interface{}{"enabled": true, "max_additions": 2, "price_requirement": 0.01},
|
||||
DescZH: "只在盈利仓位上加仓,最多加2次,价格需比平均成本高1%",
|
||||
DescZH: "Only add to winning positions, max 2 additions, price must be 1% above avg cost",
|
||||
DescEN: "Only add to winning positions, max 2 additions, price must be 1% above avg cost",
|
||||
ReasonZH: "顺势加仓,不追亏损",
|
||||
ReasonZH: "Add to winners, never average down losers",
|
||||
ReasonEN: "Add to winners, never average down losers",
|
||||
},
|
||||
"ScaleOut": {
|
||||
@@ -332,9 +332,9 @@ var TradingRules = struct {
|
||||
{"pnl": 0.05, "close_pct": 0.50},
|
||||
{"pnl": 0.08, "close_pct": 1.00},
|
||||
},
|
||||
DescZH: "分批止盈:盈利3%时平33%,5%时平50%,8%时全平",
|
||||
DescZH: "Scale-out: Close 33% at +3%, 50% at +5%, 100% at +8%",
|
||||
DescEN: "Scale-out: Close 33% at +3%, 50% at +5%, 100% at +8%",
|
||||
ReasonZH: "在保证利润的同时让盈利奔跑",
|
||||
ReasonZH: "Lock profits while letting winners run",
|
||||
ReasonEN: "Lock profits while letting winners run",
|
||||
},
|
||||
},
|
||||
@@ -367,28 +367,28 @@ var OIInterpretation = OIInterpretationType{
|
||||
ZH string
|
||||
EN string
|
||||
}{
|
||||
ZH: "强多头趋势(新多单开仓,资金流入做多)",
|
||||
ZH: "Strong bullish trend (new longs opening, capital flowing into long positions)",
|
||||
EN: "Strong bullish trend (new longs opening, capital flowing into long positions)",
|
||||
},
|
||||
OIUp_PriceDown: struct {
|
||||
ZH string
|
||||
EN string
|
||||
}{
|
||||
ZH: "强空头趋势(新空单开仓,资金流入做空)",
|
||||
ZH: "Strong bearish trend (new shorts opening, capital flowing into short positions)",
|
||||
EN: "Strong bearish trend (new shorts opening, capital flowing into short positions)",
|
||||
},
|
||||
OIDown_PriceUp: struct {
|
||||
ZH string
|
||||
EN string
|
||||
}{
|
||||
ZH: "空头平仓(空头止损离场,可能出现反转)",
|
||||
ZH: "Shorts covering (shorts stopped out, potential reversal)",
|
||||
EN: "Shorts covering (shorts stopped out, potential reversal)",
|
||||
},
|
||||
OIDown_PriceDown: struct {
|
||||
ZH string
|
||||
EN string
|
||||
}{
|
||||
ZH: "多头平仓(多头止损离场,可能出现反转)",
|
||||
ZH: "Longs closing (longs stopped out, potential reversal)",
|
||||
EN: "Longs closing (longs stopped out, potential reversal)",
|
||||
},
|
||||
}
|
||||
@@ -407,35 +407,35 @@ type CommonMistake struct {
|
||||
|
||||
var CommonMistakes = []CommonMistake{
|
||||
{
|
||||
ErrorZH: "混淆已实现盈亏和未实现盈亏",
|
||||
ErrorZH: "Confusing realized and unrealized P&L",
|
||||
ErrorEN: "Confusing realized and unrealized P&L",
|
||||
ExampleZH: "将历史交易的盈亏与当前持仓的盈亏相加",
|
||||
ExampleZH: "Adding historical trade P&L with current position P&L",
|
||||
ExampleEN: "Adding historical trade P&L with current position P&L",
|
||||
CorrectZH: "已实现盈亏已经计入账户余额,不应重复计算",
|
||||
CorrectZH: "Realized P&L is already included in account balance, don't double count",
|
||||
CorrectEN: "Realized P&L is already included in account balance, don't double count",
|
||||
},
|
||||
{
|
||||
ErrorZH: "忽略杠杆对盈亏的影响",
|
||||
ErrorZH: "Ignoring leverage's impact on P&L",
|
||||
ErrorEN: "Ignoring leverage's impact on P&L",
|
||||
ExampleZH: "价格涨1%,认为盈利1%",
|
||||
ExampleZH: "Price up 1%, thinking profit is 1%",
|
||||
ExampleEN: "Price up 1%, thinking profit is 1%",
|
||||
CorrectZH: "3x杠杆时,价格涨1%,实际盈利约3%",
|
||||
CorrectZH: "With 3x leverage, 1% price move = ~3% P&L",
|
||||
CorrectEN: "With 3x leverage, 1% price move = ~3% P&L",
|
||||
},
|
||||
{
|
||||
ErrorZH: "不理解Peak PnL的重要性",
|
||||
ErrorZH: "Not understanding Peak PnL's importance",
|
||||
ErrorEN: "Not understanding Peak PnL's importance",
|
||||
ExampleZH: "只关注当前PnL,不关注回撤",
|
||||
ExampleZH: "Only watching current PnL, ignoring drawdown",
|
||||
ExampleEN: "Only watching current PnL, ignoring drawdown",
|
||||
CorrectZH: "当前PnL接近Peak PnL时,应考虑止盈以锁定利润",
|
||||
CorrectZH: "When current PnL near Peak PnL, consider taking profit to lock in gains",
|
||||
CorrectEN: "When current PnL near Peak PnL, consider taking profit to lock in gains",
|
||||
},
|
||||
{
|
||||
ErrorZH: "忽略持仓量(OI)变化",
|
||||
ErrorZH: "Ignoring Open Interest changes",
|
||||
ErrorEN: "Ignoring Open Interest changes",
|
||||
ExampleZH: "只看价格K线,不看资金流向",
|
||||
ExampleZH: "Only watching price candles, not capital flows",
|
||||
ExampleEN: "Only watching price candles, not capital flows",
|
||||
CorrectZH: "结合OI变化判断趋势的真实性和持续性",
|
||||
CorrectZH: "Use OI changes to validate trend authenticity and sustainability",
|
||||
CorrectEN: "Use OI changes to validate trend authenticity and sustainability",
|
||||
},
|
||||
}
|
||||
@@ -452,39 +452,39 @@ func GetSchemaPrompt(lang Language) string {
|
||||
|
||||
// getSchemaPromptZH generates the Chinese prompt
|
||||
func getSchemaPromptZH() string {
|
||||
prompt := "# 📖 数据字典与交易规则\n\n"
|
||||
prompt += "## 📊 字段含义说明\n\n"
|
||||
prompt := "# 📖 Data Dictionary & Trading Rules\n\n"
|
||||
prompt += "## 📊 Field Definitions\n\n"
|
||||
|
||||
// Account metrics
|
||||
prompt += "### 账户指标\n"
|
||||
prompt += "### Account Metrics\n"
|
||||
for key, field := range DataDictionary["AccountMetrics"] {
|
||||
prompt += formatFieldDefZH(key, field)
|
||||
}
|
||||
|
||||
// Trade metrics
|
||||
prompt += "\n### 交易指标\n"
|
||||
prompt += "\n### Trade Metrics\n"
|
||||
for key, field := range DataDictionary["TradeMetrics"] {
|
||||
prompt += formatFieldDefZH(key, field)
|
||||
}
|
||||
|
||||
// Position metrics
|
||||
prompt += "\n### 持仓指标\n"
|
||||
prompt += "\n### Position Metrics\n"
|
||||
for key, field := range DataDictionary["PositionMetrics"] {
|
||||
prompt += formatFieldDefZH(key, field)
|
||||
}
|
||||
|
||||
// Market data
|
||||
prompt += "\n### 市场数据\n"
|
||||
prompt += "\n### Market Data\n"
|
||||
for key, field := range DataDictionary["MarketData"] {
|
||||
prompt += formatFieldDefZH(key, field)
|
||||
}
|
||||
|
||||
// OI interpretation
|
||||
prompt += "\n## 💹 持仓量(OI)变化解读\n\n"
|
||||
prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n"
|
||||
prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n"
|
||||
prompt += "- **OI减少 + 价格上涨**: " + OIInterpretation.OIDown_PriceUp.ZH + "\n"
|
||||
prompt += "- **OI减少 + 价格下跌**: " + OIInterpretation.OIDown_PriceDown.ZH + "\n"
|
||||
prompt += "\n## 💹 Open Interest (OI) Change Interpretation\n\n"
|
||||
prompt += "- **OI Up + Price Up**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n"
|
||||
prompt += "- **OI Up + Price Down**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n"
|
||||
prompt += "- **OI Down + Price Up**: " + OIInterpretation.OIDown_PriceUp.ZH + "\n"
|
||||
prompt += "- **OI Down + Price Down**: " + OIInterpretation.OIDown_PriceDown.ZH + "\n"
|
||||
|
||||
return prompt
|
||||
}
|
||||
@@ -530,12 +530,12 @@ func getSchemaPromptEN() string {
|
||||
|
||||
// formatFieldDefZH formats a field definition in Chinese
|
||||
func formatFieldDefZH(key string, field BilingualFieldDef) string {
|
||||
result := "- **" + key + "**(" + field.NameZH + "): " + field.DescZH
|
||||
result := "- **" + key + "** (" + field.NameZH + "): " + field.DescZH
|
||||
if field.FormulaZH != "" {
|
||||
result += " | 公式: `" + field.FormulaZH + "`"
|
||||
result += " | Formula: `" + field.FormulaZH + "`"
|
||||
}
|
||||
if field.Unit != "" {
|
||||
result += " | 单位: " + field.Unit
|
||||
result += " | Unit: " + field.Unit
|
||||
}
|
||||
result += "\n"
|
||||
return result
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// Hard limits to prevent token explosion in AI requests
|
||||
const (
|
||||
MaxCandidateCoins = 10
|
||||
MaxPositions = 3
|
||||
MaxPositions = 8
|
||||
MaxTimeframes = 4
|
||||
MinKlineCount = 10
|
||||
MaxKlineCount = 30
|
||||
@@ -268,9 +268,9 @@ func (c *StrategyConfig) NormalizeProductSchema() {
|
||||
func normalizeStrategyType(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
switch value {
|
||||
case "grid", "grid_strategy", "grid-trading", "grid trading", "grid_trading", "网格", "网格策略", "网格交易":
|
||||
case "grid", "grid_strategy", "grid-trading", "grid trading", "grid_trading", "grid strategy":
|
||||
return "grid_trading"
|
||||
case "", "ai", "ai_strategy", "ai-trading", "ai trading", "ai_trading", "ai策略", "ai 策略", "ai交易策略", "ai智能策略":
|
||||
case "", "ai", "ai_strategy", "ai-trading", "ai trading", "ai_trading", "ai strategy", "ai smart strategy":
|
||||
return "ai_trading"
|
||||
default:
|
||||
return value
|
||||
@@ -279,25 +279,25 @@ func normalizeStrategyType(value string) string {
|
||||
|
||||
func normalizeCoinSourceType(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
compact := strings.NewReplacer(" ", "", "_", "", "-", "", "数据源", "", "选币", "", "币种", "").Replace(value)
|
||||
compact := strings.NewReplacer(" ", "", "_", "", "-", "", "datasource", "", "coinselection", "", "coin", "").Replace(value)
|
||||
switch {
|
||||
case compact == "":
|
||||
return ""
|
||||
case strings.Contains(compact, "ai500"):
|
||||
return "ai500"
|
||||
case strings.Contains(compact, "oitop") || strings.Contains(value, "oi top") || strings.Contains(value, "持仓量最高") || strings.Contains(value, "持仓量靠前"):
|
||||
case strings.Contains(compact, "oitop") || strings.Contains(value, "oi top") || strings.Contains(value, "highest open interest") || strings.Contains(value, "top open interest"):
|
||||
return "oi_top"
|
||||
case strings.Contains(compact, "oilow") || strings.Contains(value, "oi low") || strings.Contains(value, "持仓量最低") || strings.Contains(value, "持仓量较低"):
|
||||
case strings.Contains(compact, "oilow") || strings.Contains(value, "oi low") || strings.Contains(value, "lowest open interest") || strings.Contains(value, "low open interest"):
|
||||
return "oi_low"
|
||||
case strings.Contains(compact, "hyperrank"):
|
||||
return "hyper_rank"
|
||||
case strings.Contains(compact, "vergex") || strings.Contains(compact, "claw402") || strings.Contains(compact, "dynamicranking") || strings.Contains(value, "动态榜单") || strings.Contains(value, "涨幅榜") || strings.Contains(value, "信号榜"):
|
||||
case strings.Contains(compact, "vergex") || strings.Contains(compact, "claw402") || strings.Contains(compact, "dynamicranking") || strings.Contains(value, "dynamic board") || strings.Contains(value, "gainers board") || strings.Contains(value, "signal board"):
|
||||
return "vergex_signal"
|
||||
case strings.Contains(compact, "hyperall"):
|
||||
return "hyper_all"
|
||||
case strings.Contains(compact, "hypermain"):
|
||||
return "hyper_main"
|
||||
case strings.Contains(value, "static") || strings.Contains(value, "固定") || strings.Contains(value, "静态"):
|
||||
case strings.Contains(value, "static") || strings.Contains(value, "fixed"):
|
||||
return "static"
|
||||
default:
|
||||
return value
|
||||
@@ -395,25 +395,25 @@ func splitLooseStringList(values []string) []string {
|
||||
|
||||
func normalizeTimeframe(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
value = strings.Trim(value, "\"',,。 ")
|
||||
value = strings.Trim(value, "\"', . ")
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
aliases := map[string]string{
|
||||
"1分钟": "1m",
|
||||
"3分钟": "3m",
|
||||
"5分钟": "5m",
|
||||
"15分钟": "15m",
|
||||
"30分钟": "30m",
|
||||
"1小时": "1h",
|
||||
"2小时": "2h",
|
||||
"4小时": "4h",
|
||||
"6小时": "6h",
|
||||
"8小时": "8h",
|
||||
"12小时": "12h",
|
||||
"1天": "1d",
|
||||
"3天": "3d",
|
||||
"1周": "1w",
|
||||
"1 minute": "1m",
|
||||
"3 minute": "3m",
|
||||
"5 minute": "5m",
|
||||
"15 minute": "15m",
|
||||
"30 minute": "30m",
|
||||
"1 hour": "1h",
|
||||
"2 hour": "2h",
|
||||
"4 hour": "4h",
|
||||
"6 hour": "6h",
|
||||
"8 hour": "8h",
|
||||
"12 hour": "12h",
|
||||
"1 day": "1d",
|
||||
"3 day": "3d",
|
||||
"1 week": "1w",
|
||||
}
|
||||
if alias, ok := aliases[value]; ok {
|
||||
return alias
|
||||
@@ -559,7 +559,7 @@ func StrategyClampWarnings(before, after StrategyConfig, lang string) []string {
|
||||
return
|
||||
}
|
||||
if lang == "zh" {
|
||||
warnings = append(warnings, fmt.Sprintf("%s 已从 %d 调整为 %d", labelZH, from, to))
|
||||
warnings = append(warnings, fmt.Sprintf("%s adjusted from %d to %d", labelZH, from, to))
|
||||
return
|
||||
}
|
||||
warnings = append(warnings, fmt.Sprintf("%s adjusted from %d to %d", labelEN, from, to))
|
||||
@@ -569,21 +569,21 @@ func StrategyClampWarnings(before, after StrategyConfig, lang string) []string {
|
||||
return
|
||||
}
|
||||
if lang == "zh" {
|
||||
warnings = append(warnings, fmt.Sprintf("%s 已从 %.2f 调整为 %.2f", labelZH, from, to))
|
||||
warnings = append(warnings, fmt.Sprintf("%s adjusted from %.2f to %.2f", labelZH, from, to))
|
||||
return
|
||||
}
|
||||
warnings = append(warnings, fmt.Sprintf("%s adjusted from %.2f to %.2f", labelEN, from, to))
|
||||
}
|
||||
|
||||
appendInt("最大持仓数", "max_positions", before.RiskControl.MaxPositions, after.RiskControl.MaxPositions)
|
||||
appendInt("BTC/ETH 最大杠杆", "btc_eth_max_leverage", before.RiskControl.BTCETHMaxLeverage, after.RiskControl.BTCETHMaxLeverage)
|
||||
appendInt("山寨币最大杠杆", "altcoin_max_leverage", before.RiskControl.AltcoinMaxLeverage, after.RiskControl.AltcoinMaxLeverage)
|
||||
appendFloat("BTC/ETH 最大仓位价值倍数", "btc_eth_max_position_value_ratio", before.RiskControl.BTCETHMaxPositionValueRatio, after.RiskControl.BTCETHMaxPositionValueRatio)
|
||||
appendFloat("山寨币最大仓位价值倍数", "altcoin_max_position_value_ratio", before.RiskControl.AltcoinMaxPositionValueRatio, after.RiskControl.AltcoinMaxPositionValueRatio)
|
||||
appendFloat("最小盈亏比", "min_risk_reward_ratio", before.RiskControl.MinRiskRewardRatio, after.RiskControl.MinRiskRewardRatio)
|
||||
appendFloat("最大保证金使用率", "max_margin_usage", before.RiskControl.MaxMarginUsage, after.RiskControl.MaxMarginUsage)
|
||||
appendFloat("最小开仓金额", "min_position_size", before.RiskControl.MinPositionSize, after.RiskControl.MinPositionSize)
|
||||
appendInt("最低置信度", "min_confidence", before.RiskControl.MinConfidence, after.RiskControl.MinConfidence)
|
||||
appendInt("Max Positions", "max_positions", before.RiskControl.MaxPositions, after.RiskControl.MaxPositions)
|
||||
appendInt("BTC/ETH Max Leverage", "btc_eth_max_leverage", before.RiskControl.BTCETHMaxLeverage, after.RiskControl.BTCETHMaxLeverage)
|
||||
appendInt("Altcoin Max Leverage", "altcoin_max_leverage", before.RiskControl.AltcoinMaxLeverage, after.RiskControl.AltcoinMaxLeverage)
|
||||
appendFloat("BTC/ETH Max Position Value Ratio", "btc_eth_max_position_value_ratio", before.RiskControl.BTCETHMaxPositionValueRatio, after.RiskControl.BTCETHMaxPositionValueRatio)
|
||||
appendFloat("Altcoin Max Position Value Ratio", "altcoin_max_position_value_ratio", before.RiskControl.AltcoinMaxPositionValueRatio, after.RiskControl.AltcoinMaxPositionValueRatio)
|
||||
appendFloat("Min Risk/Reward Ratio", "min_risk_reward_ratio", before.RiskControl.MinRiskRewardRatio, after.RiskControl.MinRiskRewardRatio)
|
||||
appendFloat("Max Margin Usage", "max_margin_usage", before.RiskControl.MaxMarginUsage, after.RiskControl.MaxMarginUsage)
|
||||
appendFloat("Min Position Size", "min_position_size", before.RiskControl.MinPositionSize, after.RiskControl.MinPositionSize)
|
||||
appendInt("Min Confidence", "min_confidence", before.RiskControl.MinConfidence, after.RiskControl.MinConfidence)
|
||||
return warnings
|
||||
}
|
||||
|
||||
@@ -1014,37 +1014,37 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
||||
PriceRankingLimit: 10,
|
||||
},
|
||||
RiskControl: RiskControlConfig{
|
||||
MaxPositions: 2, // Max 2 instruments simultaneously (CODE ENFORCED)
|
||||
BTCETHMaxLeverage: 10, // BTC/ETH exchange leverage (AI guided)
|
||||
AltcoinMaxLeverage: 10, // TradeFi exchange leverage (AI guided)
|
||||
BTCETHMaxPositionValueRatio: 10.0, // Claw402 full-size 10x notional: equity × 10
|
||||
AltcoinMaxPositionValueRatio: 10.0, // Claw402 full-size 10x notional: equity × 10
|
||||
MaxMarginUsage: 1.0, // Claw402 Autopilot intentionally uses full margin when opening
|
||||
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
|
||||
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
|
||||
MinConfidence: 78, // Min 78% confidence (AI guided)
|
||||
MaxPositions: 6, // Hold up to 6 instruments (≈3 long + 3 short) simultaneously (CODE ENFORCED)
|
||||
BTCETHMaxLeverage: 10, // BTC/ETH exchange leverage (AI guided)
|
||||
AltcoinMaxLeverage: 10, // TradeFi exchange leverage (AI guided)
|
||||
BTCETHMaxPositionValueRatio: 1.2, // Per-position notional = equity × 1.2 so several positions fit the margin
|
||||
AltcoinMaxPositionValueRatio: 1.2, // Per-position notional = equity × 1.2 so several positions fit the margin
|
||||
MaxMarginUsage: 1.0, // Claw402 Autopilot intentionally uses full margin when opening
|
||||
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
|
||||
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
|
||||
MinConfidence: 78, // Min 78% confidence (AI guided)
|
||||
},
|
||||
}
|
||||
|
||||
if lang == "zh" {
|
||||
config.PromptSections = PromptSectionsConfig{
|
||||
RoleDefinition: `# 你是 NOFX Claw402 自动交易员
|
||||
RoleDefinition: `# You are the NOFX Claw402 auto-trader
|
||||
|
||||
你只交易 Claw402.ai/Vergex 本轮榜单返回的 Hyperliquid 可交易标的。候选池来自 Claw402.ai/Vergex,开仓前必须结合 Signal Lab、成本/清算热力图和原始 K 线判断。`,
|
||||
TradingFrequency: `# 交易频率
|
||||
Trade only the Hyperliquid tradable instruments returned by this cycle's Claw402.ai/Vergex board. The candidate pool comes from Claw402.ai/Vergex; before opening a position, you must combine Signal Lab, cost/liquidation heatmap and raw candles.`,
|
||||
TradingFrequency: `# Trading Frequency
|
||||
|
||||
- 优先等待高质量机会,不需要每轮都交易。
|
||||
- 先管理已有持仓,再考虑新开仓。
|
||||
- 同一轮不要频繁开平同一标的。`,
|
||||
EntryStandards: `# 入场标准
|
||||
- Prioritize waiting for high-quality opportunities; you do not need to trade every cycle.
|
||||
- Manage existing positions first, then consider opening new ones.
|
||||
- Do not churn in and out of the same symbol in one cycle.`,
|
||||
EntryStandards: `# Entry Standards
|
||||
|
||||
只有 Claw402 Signal Lab、成本/清算热力图和原始 K 线大体一致时才开仓。Claw402 排名只是候选池,不是单独买入理由。任一关键数据缺失或冲突时,默认等待。`,
|
||||
DecisionProcess: `# 决策流程
|
||||
Open a position only when Claw402 Signal Lab, cost/liquidation heatmap and raw candles broadly agree. The Claw402 ranking is only the candidate pool, not a standalone buy reason. Wait by default when any key data is missing or contradictory.`,
|
||||
DecisionProcess: `# Decision Process
|
||||
|
||||
1. 检查已有持仓,先决定止盈、止损或继续持有。
|
||||
2. 从 Claw402 榜单取本轮候选,并对每个候选读取 Claw402 Ranking、Signal Lab、Cost/Liquidation Heatmap。
|
||||
3. 用原始 K 线确认入场位置、止损和止盈。
|
||||
4. 输出简洁 reasoning 和严格 JSON。`,
|
||||
1. Check existing positions first: decide take profit, stop loss or hold.
|
||||
2. Pull this cycle's candidates from the Claw402 board, and for each candidate read Claw402 Ranking, Signal Lab and Cost/Liquidation Heatmap.
|
||||
3. Use raw candles to confirm entry, stop loss and take profit.
|
||||
4. Output concise reasoning and strict JSON.`,
|
||||
}
|
||||
} else {
|
||||
config.PromptSections = PromptSectionsConfig{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
99
trader/auto_trader_force.go
Normal file
99
trader/auto_trader_force.go
Normal 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
|
||||
}
|
||||
57
trader/auto_trader_force_test.go
Normal file
57
trader/auto_trader_force_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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'
|
||||
? '一键接入 Hyperliquid、OKX、Aster 等 10+ 交易所与 7 个 LLM 模型, 用自然语言部署 24/7 自动化策略.'
|
||||
? 'Plug into 10+ exchanges including Hyperliquid, OKX, Aster, and 7 LLM models. Deploy 24/7 automated strategies with natural language.'
|
||||
: language === 'id'
|
||||
? 'Hubungkan ke 10+ bursa termasuk Hyperliquid, OKX, Aster dan 7 model LLM. Terapkan strategi otomatis 24/7 dengan bahasa alami.'
|
||||
: 'Plug into 10+ exchanges including Hyperliquid, OKX, Aster, and 7 LLM models. Deploy 24/7 automated strategies with natural language.'}
|
||||
@@ -129,7 +129,7 @@ export function LoginPage() {
|
||||
value="10+"
|
||||
label={
|
||||
language === 'zh'
|
||||
? '交易所'
|
||||
? 'Exchanges'
|
||||
: language === 'id'
|
||||
? 'Bursa'
|
||||
: 'Exchanges'
|
||||
@@ -139,7 +139,7 @@ export function LoginPage() {
|
||||
value="7"
|
||||
label={
|
||||
language === 'zh'
|
||||
? 'AI 模型'
|
||||
? 'AI Models'
|
||||
: language === 'id'
|
||||
? 'Model AI'
|
||||
: 'AI Models'
|
||||
@@ -149,7 +149,7 @@ export function LoginPage() {
|
||||
value="24/7"
|
||||
label={
|
||||
language === 'zh'
|
||||
? '全天候'
|
||||
? 'Always On'
|
||||
: language === 'id'
|
||||
? 'Sepanjang Waktu'
|
||||
: 'Always On'
|
||||
@@ -164,19 +164,19 @@ export function LoginPage() {
|
||||
{/* Mobile brand */}
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
< 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>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
? '支持 MetaMask、Rabby、Coinbase、Phantom、Brave、Backpack、OKX、Trust 等 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'
|
||||
? '续期需要先登录 NOFX:Hyperliquid 不允许重复使用同一个 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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
打开 PR:base 选择 <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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,61 +10,61 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
|
||||
const features = [
|
||||
{
|
||||
icon: Brain,
|
||||
title: language === 'zh' ? 'AI 策略编排引擎' : 'AI Strategy Orchestration',
|
||||
title: language === 'zh' ? 'AI Strategy Orchestration' : 'AI Strategy Orchestration',
|
||||
desc: language === 'zh'
|
||||
? '支持 DeepSeek、GPT、Claude、Qwen 等多种大模型,自定义 Prompt 策略,AI 自主分析市场并做出交易决策'
|
||||
? 'Support DeepSeek, GPT, Claude, Qwen and more. Custom prompts, AI autonomously analyzes markets and makes trading decisions'
|
||||
: 'Support DeepSeek, GPT, Claude, Qwen and more. Custom prompts, AI autonomously analyzes markets and makes trading decisions',
|
||||
highlight: true,
|
||||
badge: language === 'zh' ? '核心能力' : 'Core',
|
||||
badge: language === 'zh' ? 'Core' : 'Core',
|
||||
},
|
||||
{
|
||||
icon: Swords,
|
||||
title: language === 'zh' ? '多 AI 竞技场' : 'Multi-AI Arena',
|
||||
title: language === 'zh' ? 'Multi-AI Arena' : 'Multi-AI Arena',
|
||||
desc: language === 'zh'
|
||||
? '多个 AI 交易员同台竞技,实时 PnL 排行榜,自动优胜劣汰,让最强策略脱颖而出'
|
||||
? 'Multiple AI traders compete in real-time, live PnL leaderboard, automatic survival of the fittest'
|
||||
: 'Multiple AI traders compete in real-time, live PnL leaderboard, automatic survival of the fittest',
|
||||
highlight: true,
|
||||
badge: language === 'zh' ? '独创' : 'Unique',
|
||||
badge: language === 'zh' ? 'Unique' : 'Unique',
|
||||
},
|
||||
{
|
||||
icon: LineChart,
|
||||
title: language === 'zh' ? '专业量化数据' : 'Pro Quant Data',
|
||||
title: language === 'zh' ? 'Pro Quant Data' : 'Pro Quant Data',
|
||||
desc: language === 'zh'
|
||||
? '集成 K线、技术指标、市场深度、资金费率、持仓量等专业量化数据,为 AI 决策提供全面信息'
|
||||
? 'Integrated candlesticks, indicators, order book, funding rates, open interest - comprehensive data for AI decisions'
|
||||
: 'Integrated candlesticks, indicators, order book, funding rates, open interest - comprehensive data for AI decisions',
|
||||
highlight: true,
|
||||
badge: language === 'zh' ? '专业' : 'Pro',
|
||||
badge: language === 'zh' ? 'Pro' : 'Pro',
|
||||
},
|
||||
{
|
||||
icon: Blocks,
|
||||
title: language === 'zh' ? '多交易所支持' : 'Multi-Exchange Support',
|
||||
title: language === 'zh' ? 'Multi-Exchange Support' : 'Multi-Exchange Support',
|
||||
desc: language === 'zh'
|
||||
? 'Binance、OKX、Bybit、Hyperliquid、Aster DEX,一套系统管理多个交易所'
|
||||
? 'Binance, OKX, Bybit, Hyperliquid, Aster DEX - one system, multiple exchanges'
|
||||
: 'Binance, OKX, Bybit, Hyperliquid, Aster DEX - one system, multiple exchanges',
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: language === 'zh' ? '实时可视化看板' : 'Real-time Dashboard',
|
||||
title: language === 'zh' ? 'Real-time Dashboard' : 'Real-time Dashboard',
|
||||
desc: language === 'zh'
|
||||
? '交易监控、收益曲线、持仓分析、AI 决策日志,一目了然'
|
||||
? 'Trade monitoring, PnL curves, position analysis, AI decision logs at a glance'
|
||||
: 'Trade monitoring, PnL curves, position analysis, AI decision logs at a glance',
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: language === 'zh' ? '开源自托管' : 'Open Source & Self-Hosted',
|
||||
title: language === 'zh' ? 'Open Source & Self-Hosted' : 'Open Source & Self-Hosted',
|
||||
desc: language === 'zh'
|
||||
? '代码完全开源可审计,数据存储在本地,API 密钥不经过第三方'
|
||||
? 'Fully open source, data stored locally, API keys never leave your server'
|
||||
: 'Fully open source, data stored locally, API keys never leave your server',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>> CONNECTING TO MARKET DATA... OK</div>
|
||||
<div>> SYNCING VENUES (424/424)... OK</div>
|
||||
<div>> 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
75
web/src/components/terminal/Candles.tsx
Normal file
75
web/src/components/terminal/Candles.tsx
Normal 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
|
||||
300
web/src/components/terminal/ExecutionLog.tsx
Normal file
300
web/src/components/terminal/ExecutionLog.tsx
Normal 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
|
||||
150
web/src/components/terminal/FlowMarkets.tsx
Normal file
150
web/src/components/terminal/FlowMarkets.tsx
Normal 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
|
||||
166
web/src/components/terminal/KlineChart.tsx
Normal file
166
web/src/components/terminal/KlineChart.tsx
Normal 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
|
||||
236
web/src/components/terminal/LiquidationMap.tsx
Normal file
236
web/src/components/terminal/LiquidationMap.tsx
Normal 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
|
||||
213
web/src/components/terminal/OrchestrationTopology.tsx
Normal file
213
web/src/components/terminal/OrchestrationTopology.tsx
Normal 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
|
||||
311
web/src/components/terminal/OrderBook.tsx
Normal file
311
web/src/components/terminal/OrderBook.tsx
Normal 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
|
||||
383
web/src/components/terminal/RiskRadar.tsx
Normal file
383
web/src/components/terminal/RiskRadar.tsx
Normal 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
|
||||
192
web/src/components/terminal/SignalMatrix.tsx
Normal file
192
web/src/components/terminal/SignalMatrix.tsx
Normal 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
|
||||
566
web/src/components/terminal/TerminalDashboard.tsx
Normal file
566
web/src/components/terminal/TerminalDashboard.tsx
Normal 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
Reference in New Issue
Block a user