mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-19 02:14:35 +08:00
Compare commits
2 Commits
feature/st
...
palette/lo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f63eab72f7 | ||
|
|
6c26f9eb95 |
3
.Jules/palette.md
Normal file
3
.Jules/palette.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 2025-05-22 - [Accessibility: Password Toggles]
|
||||
**Learning:** Icon-only buttons for password visibility toggles are a common accessibility gap. Adding `aria-label` and `aria-pressed` ensures screen reader users understand the button's purpose and state.
|
||||
**Action:** Always check password fields and add these attributes along with corresponding translations in `translations.ts`.
|
||||
@@ -3372,7 +3372,7 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
|
||||
{"id": "deepseek", "name": "DeepSeek", "provider": "deepseek", "defaultModel": "deepseek-chat"},
|
||||
{"id": "qwen", "name": "Qwen", "provider": "qwen", "defaultModel": "qwen3-max"},
|
||||
{"id": "openai", "name": "OpenAI", "provider": "openai", "defaultModel": "gpt-5.1"},
|
||||
{"id": "claude", "name": "Claude", "provider": "claude", "defaultModel": "claude-opus-4-6"},
|
||||
{"id": "claude", "name": "Claude", "provider": "claude", "defaultModel": "claude-opus-4-5-20251101"},
|
||||
{"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3-pro-preview"},
|
||||
{"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"},
|
||||
{"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"},
|
||||
|
||||
@@ -75,7 +75,7 @@ func main() {
|
||||
fmt.Printf("ERROR: Failed to create TxClient: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("SUCCESS: TxClient created\n")
|
||||
fmt.Println("SUCCESS: TxClient created")
|
||||
|
||||
// Step 3: Generate auth token
|
||||
fmt.Println("Step 3: Generating auth token...")
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
const (
|
||||
ProviderClaude = "claude"
|
||||
DefaultClaudeBaseURL = "https://api.anthropic.com/v1"
|
||||
DefaultClaudeModel = "claude-opus-4-6"
|
||||
DefaultClaudeModel = "claude-opus-4-5-20251101"
|
||||
)
|
||||
|
||||
type ClaudeClient struct {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -3,63 +3,12 @@ package store
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// adaptivePriceRound rounds a price based on its magnitude to preserve meaningful precision.
|
||||
// For small prices (like meme coins), it preserves more decimal places.
|
||||
// It detects the number of decimal places needed from the reference price(s).
|
||||
func adaptivePriceRound(price float64, referencePrices ...float64) float64 {
|
||||
if price == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Find the minimum magnitude among all prices (including the price itself)
|
||||
minMagnitude := math.Abs(price)
|
||||
for _, ref := range referencePrices {
|
||||
if ref > 0 && ref < minMagnitude {
|
||||
minMagnitude = ref
|
||||
}
|
||||
}
|
||||
|
||||
// Determine decimal places needed based on price magnitude
|
||||
// For price 0.000000541, we need ~15 decimal places
|
||||
// For price 0.0001, we need ~8 decimal places
|
||||
// For price 1.0, we need ~4 decimal places
|
||||
var multiplier float64
|
||||
switch {
|
||||
case minMagnitude < 0.000001: // Ultra small (meme coins like CHEEMS, SHIB)
|
||||
multiplier = 1e15 // 15 decimal places
|
||||
case minMagnitude < 0.0001: // Very small (PEPE, FLOKI)
|
||||
multiplier = 1e12 // 12 decimal places
|
||||
case minMagnitude < 0.01: // Small
|
||||
multiplier = 1e10 // 10 decimal places
|
||||
case minMagnitude < 1: // Medium
|
||||
multiplier = 1e8 // 8 decimal places
|
||||
default: // Large
|
||||
multiplier = 1e6 // 6 decimal places
|
||||
}
|
||||
|
||||
return math.Round(price*multiplier) / multiplier
|
||||
}
|
||||
|
||||
// getPriceDecimalPlaces returns the number of decimal places in a price string
|
||||
func getPriceDecimalPlaces(price float64) int {
|
||||
if price == 0 {
|
||||
return 0
|
||||
}
|
||||
s := strconv.FormatFloat(price, 'f', -1, 64)
|
||||
idx := strings.Index(s, ".")
|
||||
if idx == -1 {
|
||||
return 0
|
||||
}
|
||||
return len(s) - idx - 1
|
||||
}
|
||||
|
||||
// TraderStats trading statistics metrics
|
||||
type TraderStats struct {
|
||||
TotalTrades int `json:"total_trades"`
|
||||
@@ -207,8 +156,8 @@ func (s *PositionStore) UpdatePositionQuantityAndPrice(id int64, addQty float64,
|
||||
newQty := math.Round((pos.Quantity+addQty)*10000) / 10000
|
||||
newEntryQty := math.Round((currentEntryQty+addQty)*10000) / 10000
|
||||
newEntryPrice := (pos.EntryPrice*pos.Quantity + addPrice*addQty) / newQty
|
||||
// Use adaptive precision based on price magnitude (for meme coins with very small prices)
|
||||
newEntryPrice = adaptivePriceRound(newEntryPrice, pos.EntryPrice, addPrice)
|
||||
// Use 8 decimal places for price precision (crypto standard)
|
||||
newEntryPrice = math.Round(newEntryPrice*100000000) / 100000000
|
||||
newFee := pos.Fee + addFee
|
||||
nowMs := time.Now().UTC().UnixMilli()
|
||||
|
||||
@@ -239,8 +188,8 @@ func (s *PositionStore) ReducePositionQuantity(id int64, reduceQty float64, exit
|
||||
var newExitPrice float64
|
||||
if newClosedQty > 0 {
|
||||
newExitPrice = (pos.ExitPrice*closedQty + exitPrice*reduceQty) / newClosedQty
|
||||
// Use adaptive precision based on price magnitude (for meme coins with very small prices)
|
||||
newExitPrice = adaptivePriceRound(newExitPrice, pos.ExitPrice, exitPrice, pos.EntryPrice)
|
||||
// Use 8 decimal places for price precision (crypto standard)
|
||||
newExitPrice = math.Round(newExitPrice*100000000) / 100000000
|
||||
}
|
||||
|
||||
nowMs := time.Now().UTC().UnixMilli()
|
||||
|
||||
@@ -147,8 +147,8 @@ func (pb *PositionBuilder) handleClose(
|
||||
var finalExitPrice float64
|
||||
if totalClosed > 0 {
|
||||
finalExitPrice = (position.ExitPrice*closedBefore + price*closeQty) / totalClosed
|
||||
// Use adaptive precision based on price magnitude (for meme coins with very small prices)
|
||||
finalExitPrice = adaptivePriceRound(finalExitPrice, position.ExitPrice, price, position.EntryPrice)
|
||||
// Use 8 decimal places for price precision (crypto standard)
|
||||
finalExitPrice = math.Round(finalExitPrice*100000000) / 100000000
|
||||
} else {
|
||||
finalExitPrice = price
|
||||
}
|
||||
|
||||
@@ -1982,7 +1982,7 @@ func (at *AutoTrader) recordAndConfirmOrder(orderResult map[string]interface{},
|
||||
// Exchanges with OrderSync: Skip immediate order recording, let OrderSync handle it
|
||||
// This ensures accurate data from GetTrades API and avoids duplicate records
|
||||
switch at.exchange {
|
||||
case "binance", "lighter", "hyperliquid", "bybit", "okx", "bitget", "aster", "kucoin", "gate":
|
||||
case "binance", "lighter", "hyperliquid", "bybit", "okx", "bitget", "aster", "kucoin":
|
||||
logger.Infof(" 📝 Order submitted (id: %s), will be synced by OrderSync", orderID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,11 +60,7 @@ func (t *GateTrader) GetTrades(startTime time.Time, limit int) ([]GateTrade, err
|
||||
continue
|
||||
}
|
||||
|
||||
fillPrice, err := strconv.ParseFloat(trade.Price, 64)
|
||||
if err != nil || fillPrice == 0 {
|
||||
logger.Infof("⚠️ Gate trade %d: fillPrice parse issue - raw='%s' parsed=%.8f err=%v",
|
||||
trade.Id, trade.Price, fillPrice, err)
|
||||
}
|
||||
fillPrice, _ := strconv.ParseFloat(trade.Price, 64)
|
||||
|
||||
// Get quanto_multiplier for this contract to convert size to base currency
|
||||
quantoMultiplier := 1.0
|
||||
@@ -180,6 +176,12 @@ func (t *GateTrader) SyncOrdersFromGate(traderID string, exchangeID string, exch
|
||||
syncedCount := 0
|
||||
|
||||
for _, trade := range trades {
|
||||
// Check if trade already exists (use exchangeID which is UUID, not exchange type)
|
||||
existing, err := orderStore.GetOrderByExchangeID(exchangeID, trade.TradeID)
|
||||
if err == nil && existing != nil {
|
||||
continue // Order already exists, skip
|
||||
}
|
||||
|
||||
// Normalize symbol (Gate uses BTC_USDT, normalize to BTCUSDT)
|
||||
symbol := market.Normalize(strings.ReplaceAll(trade.Symbol, "_", ""))
|
||||
|
||||
@@ -189,30 +191,11 @@ func (t *GateTrader) SyncOrdersFromGate(traderID string, exchangeID string, exch
|
||||
positionSide = "SHORT"
|
||||
}
|
||||
|
||||
execTimeMs := trade.ExecTime.UTC().UnixMilli()
|
||||
|
||||
// Check if trade already exists (use exchangeID which is UUID, not exchange type)
|
||||
existing, err := orderStore.GetOrderByExchangeID(exchangeID, trade.TradeID)
|
||||
if err == nil && existing != nil {
|
||||
// Order exists, but still try to update position for close trades
|
||||
// This handles the case where order was created but position update failed
|
||||
if strings.HasPrefix(trade.OrderAction, "close_") && trade.FillPrice > 0 {
|
||||
if err := posBuilder.ProcessTrade(
|
||||
traderID, exchangeID, exchangeType,
|
||||
symbol, positionSide, trade.OrderAction,
|
||||
trade.FillQty, trade.FillPrice, trade.Fee, trade.ProfitLoss,
|
||||
execTimeMs, trade.TradeID,
|
||||
); err != nil {
|
||||
logger.Infof(" ⚠️ Retry position update for existing trade %s failed: %v", trade.TradeID, err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Normalize side for storage
|
||||
side := strings.ToUpper(trade.Side)
|
||||
|
||||
// Create order record
|
||||
// Create order record - use UTC time in milliseconds to avoid timezone issues
|
||||
execTimeMs := trade.ExecTime.UTC().UnixMilli()
|
||||
orderRecord := &store.TraderOrder{
|
||||
TraderID: traderID,
|
||||
ExchangeID: exchangeID, // UUID
|
||||
@@ -265,20 +248,15 @@ func (t *GateTrader) SyncOrdersFromGate(traderID string, exchangeID string, exch
|
||||
}
|
||||
|
||||
// Create/update position record using PositionBuilder
|
||||
// Debug: Log the price being passed to ensure it's not 0
|
||||
if trade.FillPrice <= 0 {
|
||||
logger.Infof(" ⚠️ WARNING: trade %s has FillPrice=%.10f (invalid), skipping position update", trade.TradeID, trade.FillPrice)
|
||||
if err := posBuilder.ProcessTrade(
|
||||
traderID, exchangeID, exchangeType,
|
||||
symbol, positionSide, trade.OrderAction,
|
||||
trade.FillQty, trade.FillPrice, trade.Fee, trade.ProfitLoss,
|
||||
execTimeMs, trade.TradeID,
|
||||
); err != nil {
|
||||
logger.Infof(" ⚠️ Failed to sync position for trade %s: %v", trade.TradeID, err)
|
||||
} else {
|
||||
if err := posBuilder.ProcessTrade(
|
||||
traderID, exchangeID, exchangeType,
|
||||
symbol, positionSide, trade.OrderAction,
|
||||
trade.FillQty, trade.FillPrice, trade.Fee, trade.ProfitLoss,
|
||||
execTimeMs, trade.TradeID,
|
||||
); err != nil {
|
||||
logger.Infof(" ⚠️ Failed to sync position for trade %s: %v", trade.TradeID, err)
|
||||
} else {
|
||||
logger.Infof(" 📍 Position updated for trade: %s (action: %s, qty: %.6f, price: %.10f)", trade.TradeID, trade.OrderAction, trade.FillQty, trade.FillPrice)
|
||||
}
|
||||
logger.Infof(" 📍 Position updated for trade: %s (action: %s, qty: %.6f)", trade.TradeID, trade.OrderAction, trade.FillQty)
|
||||
}
|
||||
|
||||
syncedCount++
|
||||
|
||||
@@ -283,7 +283,7 @@ func TestNewHyperliquidTrader(t *testing.T) {
|
||||
walletAddr: "0x1234567890123456789012345678901234567890",
|
||||
testnet: true,
|
||||
wantError: true,
|
||||
errorContains: "Failed to parse private key",
|
||||
errorContains: "failed to parse private key",
|
||||
},
|
||||
{
|
||||
name: "Empty wallet address",
|
||||
|
||||
@@ -77,7 +77,7 @@ const AI_PROVIDER_CONFIG: Record<string, {
|
||||
apiName: 'OpenAI',
|
||||
},
|
||||
claude: {
|
||||
defaultModel: 'claude-opus-4-6',
|
||||
defaultModel: 'claude-opus-4-5-20251101',
|
||||
apiUrl: 'https://console.anthropic.com/settings/keys',
|
||||
apiName: 'Anthropic',
|
||||
},
|
||||
|
||||
@@ -334,6 +334,8 @@ export function LoginPage() {
|
||||
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"
|
||||
aria-label={showPassword ? t('hidePassword', language) : t('showPassword', language)}
|
||||
aria-pressed={showPassword}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { api } from '../lib/api'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { t } from '../i18n/translations'
|
||||
import { MetricTooltip } from './MetricTooltip'
|
||||
import { formatPrice, formatQuantity } from '../utils/format'
|
||||
import type {
|
||||
HistoricalPosition,
|
||||
TraderStats,
|
||||
@@ -15,7 +14,7 @@ interface PositionHistoryProps {
|
||||
traderId: string
|
||||
}
|
||||
|
||||
// Format number with proper decimals (for large numbers)
|
||||
// Format number with proper decimals
|
||||
function formatNumber(value: number, decimals: number = 2): string {
|
||||
if (Math.abs(value) >= 1000000) {
|
||||
return (value / 1000000).toFixed(2) + 'M'
|
||||
@@ -26,6 +25,14 @@ function formatNumber(value: number, decimals: number = 2): string {
|
||||
return value.toFixed(decimals)
|
||||
}
|
||||
|
||||
// Format price with proper decimals
|
||||
function formatPrice(price: number): string {
|
||||
if (!price || price === 0) return '-'
|
||||
if (price >= 1000) return price.toFixed(2)
|
||||
if (price >= 1) return price.toFixed(4)
|
||||
return price.toFixed(6)
|
||||
}
|
||||
|
||||
// Format duration from minutes
|
||||
function formatDuration(minutes: number): string {
|
||||
if (!minutes || minutes <= 0) return '-'
|
||||
@@ -293,7 +300,7 @@ function PositionRow({ position }: { position: HistoricalPosition }) {
|
||||
|
||||
{/* Quantity */}
|
||||
<td className="py-3 px-4 text-right font-mono" style={{ color: '#848E9C' }}>
|
||||
{formatQuantity(displayQty)}
|
||||
{displayQty.toFixed(4)}
|
||||
</td>
|
||||
|
||||
{/* Position Value (Entry Price * Quantity) */}
|
||||
|
||||
@@ -125,7 +125,7 @@ export function TraderConfigModal({
|
||||
|
||||
const handleFetchCurrentBalance = async () => {
|
||||
if (!isEditMode || !traderData?.trader_id) {
|
||||
setBalanceFetchError(t('fetchBalanceEditModeOnly', language))
|
||||
setBalanceFetchError('只有在编辑模式下才能获取当前余额')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,13 +142,13 @@ export function TraderConfigModal({
|
||||
const currentBalance =
|
||||
result.data.total_equity || result.data.balance || 0
|
||||
setFormData((prev) => ({ ...prev, initial_balance: currentBalance }))
|
||||
toast.success(t('balanceFetched', language))
|
||||
toast.success('已获取当前余额')
|
||||
} else {
|
||||
throw new Error(result.message || t('balanceFetchFailed', language))
|
||||
throw new Error(result.message || '获取余额失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t('balanceFetchFailed', language) + ':', error)
|
||||
setBalanceFetchError(t('balanceFetchNetworkError', language))
|
||||
console.error('获取余额失败:', error)
|
||||
setBalanceFetchError('获取余额失败,请检查网络连接')
|
||||
} finally {
|
||||
setIsFetchingBalance(false)
|
||||
}
|
||||
@@ -175,13 +175,13 @@ export function TraderConfigModal({
|
||||
}
|
||||
|
||||
await toast.promise(onSave(saveData), {
|
||||
loading: t('saving', language),
|
||||
success: t('saveSuccess', language),
|
||||
error: t('saveFailed', language),
|
||||
loading: '正在保存…',
|
||||
success: '保存成功',
|
||||
error: '保存失败',
|
||||
})
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error(t('saveFailed', language) + ':', error)
|
||||
console.error('保存失败:', error)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -208,10 +208,10 @@ export function TraderConfigModal({
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-[#EAECEF]">
|
||||
{isEditMode ? t('editTrader', language) : t('createTrader', language)}
|
||||
{isEditMode ? '修改交易员' : '创建交易员'}
|
||||
</h2>
|
||||
<p className="text-sm text-[#848E9C] mt-1">
|
||||
{isEditMode ? t('editTraderConfig', language) : t('selectStrategyAndConfigParams', language)}
|
||||
{isEditMode ? '修改交易员配置' : '选择策略并配置基础参数'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,12 +231,12 @@ export function TraderConfigModal({
|
||||
{/* Basic Info */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||
<span className="text-[#F0B90B]">1</span> {t('basicConfig', language)}
|
||||
<span className="text-[#F0B90B]">1</span> 基础配置
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{t('traderNameRequired', language)}
|
||||
交易员名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -245,13 +245,13 @@ export function TraderConfigModal({
|
||||
handleInputChange('trader_name', e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||
placeholder={t('enterTraderNamePlaceholder', language)}
|
||||
placeholder="请输入交易员名称"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{t('aiModelRequired', language)}
|
||||
AI模型 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData.ai_model}
|
||||
@@ -269,7 +269,7 @@ export function TraderConfigModal({
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{t('exchangeRequired', language)}
|
||||
交易所 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData.exchange_id}
|
||||
@@ -300,10 +300,10 @@ export function TraderConfigModal({
|
||||
className="mt-2 inline-flex items-center gap-1.5 text-xs text-[#848E9C] hover:text-[#F0B90B] transition-colors"
|
||||
>
|
||||
<UserPlus className="w-3.5 h-3.5" />
|
||||
<span>{t('noExchangeAccount', language)}</span>
|
||||
<span>还没有交易所账号?点击注册</span>
|
||||
{regLink.hasReferral && (
|
||||
<span className="px-1.5 py-0.5 bg-[#F0B90B]/10 text-[#F0B90B] rounded text-[10px]">
|
||||
{t('discount', language)}
|
||||
折扣优惠
|
||||
</span>
|
||||
)}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
@@ -318,13 +318,13 @@ export function TraderConfigModal({
|
||||
{/* Strategy Selection */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||
<span className="text-[#F0B90B]">2</span> {t('selectTradingStrategy', language)}
|
||||
<span className="text-[#F0B90B]">2</span> 选择交易策略
|
||||
<Sparkles className="w-4 h-4 text-[#F0B90B]" />
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{t('useStrategy', language)}
|
||||
使用策略
|
||||
</label>
|
||||
<select
|
||||
value={formData.strategy_id}
|
||||
@@ -333,18 +333,18 @@ export function TraderConfigModal({
|
||||
}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
|
||||
>
|
||||
<option value="">{t('noStrategyManual', language)}</option>
|
||||
<option value="">-- 不使用策略(手动配置)--</option>
|
||||
{strategies.map((strategy) => (
|
||||
<option key={strategy.id} value={strategy.id}>
|
||||
{selectedStrategy.name}
|
||||
{selectedStrategy.is_active ? t('active', language) : ''}
|
||||
{selectedStrategy.is_default ? t('default', language) : ''}
|
||||
{strategy.name}
|
||||
{strategy.is_active ? ' (当前激活)' : ''}
|
||||
{strategy.is_default ? ' [默认]' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{strategies.length === 0 && (
|
||||
<p className="text-xs text-[#848E9C] mt-2">
|
||||
{t('noStrategyHint', language)}
|
||||
<p className="text-xs text-[#848E9C] mt-2">
|
||||
暂无策略,请先在策略工作室创建策略
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -354,25 +354,25 @@ export function TraderConfigModal({
|
||||
<div className="mt-3 p-4 bg-[#1E2329] border border-[#2B3139] rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[#F0B90B] text-sm font-medium">
|
||||
{t('strategyDetails', language)}
|
||||
策略详情
|
||||
</span>
|
||||
{selectedStrategy.is_active && (
|
||||
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded">
|
||||
{t('activating', language)}
|
||||
激活中
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-[#848E9C] mb-2">
|
||||
{selectedStrategy.description || (language === 'zh' ? '无描述' : 'No description')}
|
||||
{selectedStrategy.description || '无描述'}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-[#848E9C]">
|
||||
<div>
|
||||
{t('coinSource', language)}: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' :
|
||||
币种来源: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' :
|
||||
selectedStrategy.config.coin_source.source_type === 'ai500' ? 'AI500' :
|
||||
selectedStrategy.config.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
|
||||
</div>
|
||||
<div>
|
||||
{t('marginLimit', language)}: {((selectedStrategy.config.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%
|
||||
保证金上限: {((selectedStrategy.config.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -383,13 +383,13 @@ export function TraderConfigModal({
|
||||
{/* Trading Parameters */}
|
||||
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
|
||||
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
|
||||
<span className="text-[#F0B90B]">3</span> {t('tradingParams', language)}
|
||||
<span className="text-[#F0B90B]">3</span> 交易参数
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{t('marginMode', language)}
|
||||
保证金模式
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -401,7 +401,7 @@ export function TraderConfigModal({
|
||||
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
|
||||
}`}
|
||||
>
|
||||
{t('crossMargin', language)}
|
||||
全仓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -414,7 +414,7 @@ export function TraderConfigModal({
|
||||
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
|
||||
}`}
|
||||
>
|
||||
{t('isolatedMargin', language)}
|
||||
逐仓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -446,7 +446,7 @@ export function TraderConfigModal({
|
||||
{/* Competition visibility */}
|
||||
<div>
|
||||
<label className="text-sm text-[#EAECEF] block mb-2">
|
||||
{t('competitionDisplay', language)}
|
||||
竞技场显示
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -458,7 +458,7 @@ export function TraderConfigModal({
|
||||
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
|
||||
}`}
|
||||
>
|
||||
{t('show', language)}
|
||||
显示
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -469,11 +469,11 @@ export function TraderConfigModal({
|
||||
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
|
||||
}`}
|
||||
>
|
||||
{t('hide', language)}
|
||||
隐藏
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-[#848E9C] mt-1">
|
||||
{t('hiddenInCompetition', language)}
|
||||
<p className="text-xs text-[#848E9C] mt-1">
|
||||
隐藏后将不在竞技场页面显示此交易员
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -482,7 +482,7 @@ export function TraderConfigModal({
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm text-[#EAECEF]">
|
||||
{t('initialBalanceLabel', language)}
|
||||
初始余额 ($)
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@@ -490,7 +490,7 @@ export function TraderConfigModal({
|
||||
disabled={isFetchingBalance}
|
||||
className="px-3 py-1 text-xs bg-[#F0B90B] text-black rounded hover:bg-[#E1A706] transition-colors disabled:bg-[#848E9C] disabled:cursor-not-allowed"
|
||||
>
|
||||
{isFetchingBalance ? t('fetching', language) : t('fetchCurrentBalance', language)}
|
||||
{isFetchingBalance ? '获取中...' : '获取当前余额'}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
@@ -506,8 +506,8 @@ export function TraderConfigModal({
|
||||
min="100"
|
||||
step="0.01"
|
||||
/>
|
||||
<p className="text-xs text-[#848E9C] mt-1">
|
||||
{t('balanceUpdateHint', language)}
|
||||
<p className="text-xs text-[#848E9C] mt-1">
|
||||
用于手动更新初始余额基准(例如充值/提现后)
|
||||
</p>
|
||||
{balanceFetchError && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
@@ -535,7 +535,7 @@ export function TraderConfigModal({
|
||||
<line x1="12" x2="12.01" y1="16" y2="16" />
|
||||
</svg>
|
||||
<span className="text-sm text-[#848E9C]">
|
||||
{t('autoFetchBalanceInfo', language)}
|
||||
系统将自动获取您的账户净值作为初始余额
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -550,7 +550,7 @@ export function TraderConfigModal({
|
||||
onClick={onClose}
|
||||
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
|
||||
>
|
||||
{t('cancel', language)}
|
||||
取消
|
||||
</button>
|
||||
{onSave && (
|
||||
<button
|
||||
@@ -563,7 +563,7 @@ export function TraderConfigModal({
|
||||
}
|
||||
className="px-8 py-3 bg-gradient-to-r from-[#F0B90B] to-[#E1A706] text-black rounded-lg hover:from-[#E1A706] hover:to-[#D4951E] transition-all duration-200 disabled:bg-[#848E9C] disabled:cursor-not-allowed font-medium shadow-lg"
|
||||
>
|
||||
{isSaving ? t('saving', language) : isEditMode ? t('editTrader', language) : t('createTraderButton', language)}
|
||||
{isSaving ? '保存中...' : isEditMode ? '保存修改' : '创建交易员'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
export default function AgentTerminal() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30, rotate: 0 }}
|
||||
animate={{ opacity: 1, y: 0, rotate: 2 }}
|
||||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
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)'
|
||||
}} />
|
||||
|
||||
{/* Header bar - macOS style */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-[#0D1117] border-b border-zinc-800/60">
|
||||
{/* 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>
|
||||
</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 Agent 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>
|
||||
</div>
|
||||
|
||||
{/* Portfolio PnL Section */}
|
||||
<div className="p-4 border-b border-zinc-800/40">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-zinc-500 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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* Chart Area */}
|
||||
<div className="mt-4 h-16 rounded-lg overflow-hidden relative">
|
||||
<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" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M0,56 C40,52 80,48 120,40 C160,32 200,28 240,24 C280,20 320,16 360,12 L400,8 L400,64 L0,64 Z"
|
||||
fill="url(#chartGradient)"
|
||||
/>
|
||||
<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"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Row */}
|
||||
<div className="grid grid-cols-3 divide-x divide-zinc-800/40 border-b border-zinc-800/40">
|
||||
<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>
|
||||
<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>
|
||||
<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="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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Book */}
|
||||
<div className="p-4 border-b border-zinc-800/40">
|
||||
<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>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Asks */}
|
||||
<div className="space-y-1">
|
||||
{[
|
||||
{ price: '97,289.50', amount: '2.451', depth: 70 },
|
||||
{ price: '97,267.00', amount: '1.832', depth: 55 },
|
||||
{ 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>
|
||||
))}
|
||||
</div>
|
||||
{/* Bids */}
|
||||
<div className="space-y-1">
|
||||
{[
|
||||
{ price: '97,244.50', amount: '3.127', depth: 85 },
|
||||
{ price: '97,221.00', amount: '4.592', depth: 100 },
|
||||
{ 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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ coin: 'BTC', name: 'BTC-PERP', size: '0.5', profit: '+$6,420', percent: '+12.8%', color: '#F7931A' },
|
||||
{ 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 className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold border"
|
||||
style={{
|
||||
backgroundColor: pos.color + '15',
|
||||
borderColor: pos.color + '30',
|
||||
color: pos.color
|
||||
}}
|
||||
>
|
||||
{pos.coin}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white 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>
|
||||
</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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</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">
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
|
||||
Connected
|
||||
</span>
|
||||
<span>Latency: 12ms</span>
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-zinc-600">
|
||||
mainnet • v2.4.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { motion } from 'framer-motion'
|
||||
import { ArrowRight, Github } from 'lucide-react'
|
||||
import { Marquee } from './Marquee'
|
||||
import { OFFICIAL_LINKS } from '../../../constants/branding'
|
||||
import AgentTerminal from './AgentTerminal'
|
||||
|
||||
export default function BrandHero() {
|
||||
const handleScroll = () => {
|
||||
@@ -76,25 +75,32 @@ export default function BrandHero() {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Right Visual - Agent 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]" />
|
||||
{/* Right Visual - Mascot */}
|
||||
<div className="flex-1 relative flex items-end justify-center lg:justify-end overflow-hidden">
|
||||
{/* Abstract background elements */}
|
||||
<div className="absolute top-1/4 right-0 w-[600px] h-[600px] bg-nofx-accent/20 rounded-full blur-[100px] pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-10 w-[400px] h-[400px] bg-nofx-gold/10 rounded-full blur-[80px] pointer-events-none" />
|
||||
|
||||
{/* Subtle dot grid */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.04]"
|
||||
{/* Grid Pattern */}
|
||||
<div className="absolute inset-0 opacity-20"
|
||||
style={{
|
||||
backgroundImage: 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.4) 1px, transparent 0)',
|
||||
backgroundSize: '32px 32px'
|
||||
backgroundImage: 'linear-gradient(#333 1px, transparent 1px), linear-gradient(90deg, #333 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Terminal Panel */}
|
||||
<div className="relative z-10">
|
||||
<AgentTerminal />
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1, delay: 0.2 }}
|
||||
className="relative z-10 w-full h-full flex items-end justify-center lg:justify-end lg:pr-10"
|
||||
>
|
||||
<img
|
||||
src="/images/nofx_mascot.png"
|
||||
alt="Cyberpunk Mascot"
|
||||
className="h-[80vh] object-contain drop-shadow-[0_0_50px_rgba(0,0,0,0.5)]"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { ArrowRight, Shield, Activity, CircuitBoard, Wifi, Globe, Zap, Star, GitFork, Users, MessageCircle } from 'lucide-react'
|
||||
import { ArrowRight, Shield, Activity, CircuitBoard, Cpu, Wifi, Globe, Lock, Zap, Star, GitFork, Users, MessageCircle } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useGitHubStats } from '../../../hooks/useGitHubStats'
|
||||
import AgentTerminal from '../brand/AgentTerminal'
|
||||
|
||||
export default function TerminalHero() {
|
||||
|
||||
@@ -89,10 +88,10 @@ export default function TerminalHero() {
|
||||
{/* 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" />
|
||||
|
||||
{/* Mobile Floating HUD */}
|
||||
<div className="md:hidden absolute top-24 right-4 z-0 opacity-30 pointer-events-none">
|
||||
<div className="w-20 h-20 border border-dashed border-nofx-gold/30 rounded-full animate-spin-slow flex items-center justify-center">
|
||||
<div className="w-12 h-12 border border-nofx-accent/30 rounded-full"></div>
|
||||
{/* Mobile Floating HUD - Moved to Left to avoid covering face */}
|
||||
<div className="md:hidden absolute top-24 left-4 z-0 opacity-40 pointer-events-none">
|
||||
<div className="w-24 h-24 border border-dashed border-nofx-gold/30 rounded-full animate-spin-slow flex items-center justify-center">
|
||||
<div className="w-16 h-16 border border-nofx-accent/30 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -237,25 +236,72 @@ export default function TerminalHero() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: Agent 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>
|
||||
{/* RIGHT COLUMN: HOLOGRAPHIC DISPLAY - Absolute Overlay for "Far Right" Effect on Desktop, Background on Mobile */}
|
||||
<div className="absolute top-20 md:top-0 right-0 h-[50vh] md:h-full w-full lg:w-[45vw] flex pointer-events-none items-center justify-center z-0 opacity-80 lg:opacity-100 mix-blend-normal">
|
||||
<div className="relative w-full h-full flex items-center justify-center perspective-1000">
|
||||
{/* 3D Hologram Effect Container */}
|
||||
<div className="relative w-full h-[90%] flex items-center justify-center transform-style-3d rotate-y-[-12deg]">
|
||||
|
||||
{/* 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)',
|
||||
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%)'
|
||||
}}
|
||||
></div>
|
||||
{/* Scanning Grid behind Mascot - Mobile Optimized */}
|
||||
<div className="absolute inset-x-0 top-[10%] bottom-[10%] bg-[linear-gradient(rgba(0,240,255,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(0,240,255,0.05)_1px,transparent_1px)] bg-[size:20px_20px] [mask-image:radial-gradient(ellipse_at_center,black_40%,transparent_80%)] mobile-grid-pulse"></div>
|
||||
|
||||
{/* The Mascot Image with Glitch/Holo Effects */}
|
||||
<div className="relative z-10 w-full h-full opacity-100 transition-all duration-500 group flex flex-col justify-end pointer-events-auto">
|
||||
<div className="absolute inset-x-0 bottom-0 top-1/2 bg-nofx-accent/5 blur-[60px] rounded-full animate-pulse-slow transition-colors duration-500 group-hover:bg-nofx-gold/20"></div>
|
||||
|
||||
{/* Mobile Holo-Portrait Style - Full Color & Optimized & Premium Desktop */}
|
||||
<div className="relative w-full h-full flex items-end justify-center">
|
||||
<img
|
||||
src="/images/nofx_mascot.png"
|
||||
alt="Agent NoFX"
|
||||
className="w-full h-full object-contain object-bottom char-premium-effects animate-breath-mobile transition-all duration-500"
|
||||
style={{
|
||||
maskImage: 'radial-gradient(ellipse at center, black 60%, transparent 100%), linear-gradient(to bottom, black 0%, black 85%, transparent 100%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse at center, black 60%, transparent 100%), linear-gradient(to bottom, black 0%, black 85%, transparent 100%)',
|
||||
maskComposite: 'intersect',
|
||||
WebkitMaskComposite: 'source-in'
|
||||
}}
|
||||
/>
|
||||
{/* Dynamic Holographic Overlay - Premium Noise & Gradient */}
|
||||
<div className="absolute inset-0 w-full h-full holo-overlay animate-holo opacity-80 pointer-events-none"
|
||||
style={{
|
||||
maskImage: 'url(/images/nofx_mascot.png)',
|
||||
WebkitMaskImage: 'url(/images/nofx_mascot.png)',
|
||||
maskSize: 'contain',
|
||||
WebkitMaskSize: 'contain',
|
||||
maskPosition: 'bottom center',
|
||||
WebkitMaskPosition: 'bottom center',
|
||||
maskRepeat: 'no-repeat',
|
||||
WebkitMaskRepeat: 'no-repeat'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Holo Scan Line - Subtle on Mobile */}
|
||||
<div className="absolute w-full h-1 bg-nofx-accent/30 drop-shadow-[0_0_10px_rgba(0,240,255,0.8)] top-0 animate-scan-fast pointer-events-none mix-blend-overlay"></div>
|
||||
|
||||
{/* Mobile Glitch Overlay - Reduced Intensity */}
|
||||
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-10 mix-blend-overlay md:hidden animate-pulse-fast"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Data Widgets around Hologram */}
|
||||
<motion.div
|
||||
animate={{ y: [0, -10, 0] }}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="absolute top-[30%] left-[10%] bg-black/80 border border-nofx-accent/30 p-2 rounded backdrop-blur-md shadow-neon-blue hidden md:block"
|
||||
>
|
||||
<Cpu className="w-5 h-5 text-nofx-accent" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ y: [0, 10, 0] }}
|
||||
transition={{ duration: 5, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||||
className="absolute bottom-[20%] right-[20%] bg-black/80 border border-nofx-gold/30 p-2 rounded backdrop-blur-md shadow-neon hidden md:block"
|
||||
>
|
||||
<Lock className="w-5 h-5 text-nofx-gold" />
|
||||
</motion.div>
|
||||
|
||||
{/* Agent Terminal Panel */}
|
||||
<div className="relative z-20 pointer-events-auto">
|
||||
<AgentTerminal />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -273,7 +319,7 @@ export default function TerminalHero() {
|
||||
</span>
|
||||
))}
|
||||
|
||||
<span className="flex items-center gap-2"><CircuitBoard className="w-3 h-3 text-nofx-accent" /> AI MODEL: Claude Opus 4.6</span>
|
||||
<span className="flex items-center gap-2"><CircuitBoard className="w-3 h-3 text-nofx-accent" /> AI MODEL: GEMINI-PRO-1.5</span>
|
||||
|
||||
{/* Duplicate sequence for seamless loop effect (basic set) */}
|
||||
{Object.entries(prices).map(([symbol, price]) => (
|
||||
@@ -298,14 +344,14 @@ function CommunityStats() {
|
||||
const stats = [
|
||||
{
|
||||
label: 'GITHUB STARS',
|
||||
value: isLoading ? '...' : (error ? '10,500+' : stars.toLocaleString()),
|
||||
value: isLoading ? '...' : (error ? '9,700+' : stars.toLocaleString()),
|
||||
icon: Star,
|
||||
color: 'text-yellow-400',
|
||||
href: OFFICIAL_LINKS.github
|
||||
},
|
||||
{
|
||||
label: 'FORKS',
|
||||
value: isLoading ? '...' : (error ? '2,800+' : forks.toLocaleString()),
|
||||
value: isLoading ? '...' : (error ? '2,600+' : forks.toLocaleString()),
|
||||
icon: GitFork,
|
||||
color: 'text-blue-400',
|
||||
href: `${OFFICIAL_LINKS.github}/fork`
|
||||
@@ -319,7 +365,7 @@ function CommunityStats() {
|
||||
},
|
||||
{
|
||||
label: 'DEV COMMUNITY',
|
||||
value: '6,600+',
|
||||
value: '6,000+', // Updated as per user request
|
||||
icon: MessageCircle,
|
||||
color: 'text-blue-500',
|
||||
href: OFFICIAL_LINKS.telegram
|
||||
|
||||
@@ -83,17 +83,10 @@ export function GridConfigEditor({
|
||||
// Direction adjustment
|
||||
directionAdjust: { zh: '方向自动调整', en: 'Direction Auto-Adjust' },
|
||||
enableDirectionAdjust: { zh: '启用方向调整', en: 'Enable Direction Adjust' },
|
||||
enableDirectionAdjustDesc: { zh: '根据箱体突破自动调整网格方向', en: 'Auto-adjust grid direction based on box breakouts' },
|
||||
directionBiasRatio: { zh: '偏向强度', en: 'Bias Strength' },
|
||||
directionBiasRatioDesc: { zh: '偏多/偏空模式的强度', en: 'Strength for long_bias/short_bias modes' },
|
||||
directionBiasExplain: { zh: '偏多模式:X%买 + (100-X)%卖 | 偏空模式:(100-X)%买 + X%卖', en: 'Long bias: X% buy + (100-X)% sell | Short bias: (100-X)% buy + X% sell' },
|
||||
enableDirectionAdjustDesc: { zh: '根据箱体突破自动调整网格方向(做多/做空/偏多/偏空)', en: 'Auto-adjust grid direction based on box breakouts (long/short/long_bias/short_bias)' },
|
||||
directionBiasRatio: { zh: '偏向比例', en: 'Bias Ratio' },
|
||||
directionBiasRatioDesc: { zh: '偏多/偏空模式下的买卖比例(如 0.7 表示 70% 买 + 30% 卖)', en: 'Buy/sell ratio for bias modes (e.g., 0.7 = 70% buy + 30% sell)' },
|
||||
directionExplain: { zh: '短期箱体突破 → 偏向,中期箱体突破 → 全仓,价格回归 → 逐步恢复中性', en: 'Short box breakout → bias, Mid box breakout → full, Price return → gradually recover to neutral' },
|
||||
directionModes: { zh: '方向模式说明', en: 'Direction Modes' },
|
||||
modeNeutral: { zh: '中性:50%买 + 50%卖(默认)', en: 'Neutral: 50% buy + 50% sell (default)' },
|
||||
modeLongBias: { zh: '偏多:X%买 + (100-X)%卖', en: 'Long Bias: X% buy + (100-X)% sell' },
|
||||
modeLong: { zh: '全多:100%买 + 0%卖', en: 'Long: 100% buy + 0% sell' },
|
||||
modeShortBias: { zh: '偏空:(100-X)%买 + X%卖', en: 'Short Bias: (100-X)% buy + X% sell' },
|
||||
modeShort: { zh: '全空:0%买 + 100%卖', en: 'Short: 0% buy + 100% sell' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
@@ -472,34 +465,21 @@ export function GridConfigEditor({
|
||||
|
||||
{config.enable_direction_adjust && (
|
||||
<>
|
||||
{/* Direction Modes Explanation */}
|
||||
<div className="p-4 rounded-lg mb-4" style={{ background: '#1E2329', border: '1px solid #F0B90B33' }}>
|
||||
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
|
||||
📊 {t('directionModes')}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs" style={{ color: '#848E9C' }}>
|
||||
<div>• {t('modeNeutral')}</div>
|
||||
<div>• <span style={{ color: '#0ECB81' }}>{t('modeLongBias')}</span></div>
|
||||
<div>• <span style={{ color: '#0ECB81' }}>{t('modeLong')}</span></div>
|
||||
<div>• <span style={{ color: '#F6465D' }}>{t('modeShortBias')}</span></div>
|
||||
<div>• <span style={{ color: '#F6465D' }}>{t('modeShort')}</span></div>
|
||||
</div>
|
||||
<p className="text-xs mt-3 pt-2 border-t border-zinc-700" style={{ color: '#848E9C' }}>
|
||||
{/* Direction Explanation */}
|
||||
<div className="p-3 rounded-lg mb-4" style={{ background: '#1E2329', border: '1px solid #F0B90B33' }}>
|
||||
<p className="text-xs" style={{ color: '#F0B90B' }}>
|
||||
💡 {t('directionExplain')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bias Strength */}
|
||||
{/* Bias Ratio */}
|
||||
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{t('directionBiasRatio')} (X)
|
||||
{t('directionBiasRatio')}
|
||||
</label>
|
||||
<p className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('directionBiasRatioDesc')}
|
||||
</p>
|
||||
<p className="text-xs mb-3" style={{ color: '#F0B90B' }}>
|
||||
{t('directionBiasExplain')}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="range"
|
||||
@@ -512,20 +492,10 @@ export function GridConfigEditor({
|
||||
className="flex-1 h-2 rounded-lg appearance-none cursor-pointer"
|
||||
style={{ background: '#2B3139' }}
|
||||
/>
|
||||
<span className="text-sm font-mono w-20 text-right" style={{ color: '#F0B90B' }}>
|
||||
X = {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}%
|
||||
<span className="text-sm font-mono w-16 text-right" style={{ color: '#F0B90B' }}>
|
||||
{Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% / {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="p-2 rounded" style={{ background: '#0ECB8115', border: '1px solid #0ECB8130' }}>
|
||||
<span style={{ color: '#0ECB81' }}>偏多/Long Bias: </span>
|
||||
<span style={{ color: '#EAECEF' }}>{Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% 买 + {Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% 卖</span>
|
||||
</div>
|
||||
<div className="p-2 rounded" style={{ background: '#F6465D15', border: '1px solid #F6465D30' }}>
|
||||
<span style={{ color: '#F6465D' }}>偏空/Short Bias: </span>
|
||||
<span style={{ color: '#EAECEF' }}>{Math.round((1 - (config.direction_bias_ratio ?? 0.7)) * 100)}% 买 + {Math.round((config.direction_bias_ratio ?? 0.7) * 100)}% 卖</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -319,50 +319,6 @@ export const translations = {
|
||||
enabled: 'Enabled',
|
||||
save: 'Save',
|
||||
|
||||
// TraderConfigModal - New keys for hardcoded Chinese strings
|
||||
fetchBalanceEditModeOnly: 'Only can fetch current balance in edit mode',
|
||||
balanceFetched: 'Current balance fetched',
|
||||
balanceFetchFailed: 'Failed to fetch balance',
|
||||
balanceFetchNetworkError: 'Failed to fetch balance, please check network connection',
|
||||
saving: 'Saving...',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveFailed: 'Save failed',
|
||||
editTraderConfig: 'Edit Trader Configuration',
|
||||
selectStrategyAndConfigParams: 'Select Strategy and Configure Basic Parameters',
|
||||
basicConfig: 'Basic Configuration',
|
||||
traderNameRequired: 'Trader Name *',
|
||||
enterTraderNamePlaceholder: 'Enter trader name',
|
||||
aiModelRequired: 'AI Model *',
|
||||
exchangeRequired: 'Exchange *',
|
||||
noExchangeAccount: "Don't have an exchange account? Click to register",
|
||||
discount: 'Discount',
|
||||
selectTradingStrategy: 'Select Trading Strategy',
|
||||
useStrategy: 'Use Strategy',
|
||||
noStrategyManual: '-- No Strategy (Manual Configuration) --',
|
||||
active: ' (Active)',
|
||||
default: ' [Default]',
|
||||
noStrategyHint: 'No strategies yet, please create in Strategy Studio first',
|
||||
strategyDetails: 'Strategy Details',
|
||||
activating: 'Activating',
|
||||
coinSource: 'Coin Source',
|
||||
marginLimit: 'Margin Limit',
|
||||
tradingParams: 'Trading Parameters',
|
||||
marginMode: 'Margin Mode',
|
||||
crossMargin: 'Cross Margin',
|
||||
isolatedMargin: 'Isolated Margin',
|
||||
competitionDisplay: 'Show in Competition',
|
||||
show: 'Show',
|
||||
hide: 'Hide',
|
||||
hiddenInCompetition: 'This trader will not be shown in the competition page when hidden',
|
||||
initialBalanceLabel: 'Initial Balance ($)',
|
||||
fetching: 'Fetching...',
|
||||
fetchCurrentBalance: 'Fetch Current Balance',
|
||||
balanceUpdateHint: 'Used to manually update the initial balance baseline (e.g., after deposit/withdrawal)',
|
||||
autoFetchBalanceInfo: 'The system will automatically fetch your account equity as the initial balance',
|
||||
fetchingBalance: 'Fetching balance...',
|
||||
editTrader: 'Save Changes',
|
||||
createTraderButton: 'Create Trader',
|
||||
|
||||
// AI Model Configuration
|
||||
officialAPI: 'Official API',
|
||||
customAPI: 'Custom API',
|
||||
@@ -711,6 +667,8 @@ export const translations = {
|
||||
passwordRequired: 'Password is required',
|
||||
invalidEmail: 'Invalid email format',
|
||||
passwordTooShort: 'Password must be at least 6 characters',
|
||||
showPassword: 'Show password',
|
||||
hidePassword: 'Hide password',
|
||||
|
||||
// Landing Page
|
||||
features: 'Features',
|
||||
@@ -1567,50 +1525,6 @@ export const translations = {
|
||||
enabled: '启用',
|
||||
save: '保存',
|
||||
|
||||
// TraderConfigModal - New keys for hardcoded Chinese strings
|
||||
fetchBalanceEditModeOnly: '只有在编辑模式下才能获取当前余额',
|
||||
balanceFetched: '已获取当前余额',
|
||||
balanceFetchFailed: '获取余额失败',
|
||||
balanceFetchNetworkError: '获取余额失败,请检查网络连接',
|
||||
saving: '正在保存…',
|
||||
saveSuccess: '保存成功',
|
||||
saveFailed: '保存失败',
|
||||
editTraderConfig: '修改交易员配置',
|
||||
selectStrategyAndConfigParams: '选择策略并配置基础参数',
|
||||
basicConfig: '基础配置',
|
||||
traderNameRequired: '交易员名称 *',
|
||||
enterTraderNamePlaceholder: '请输入交易员名称',
|
||||
aiModelRequired: 'AI模型 *',
|
||||
exchangeRequired: '交易所 *',
|
||||
noExchangeAccount: '还没有交易所账号?点击注册',
|
||||
discount: '折扣优惠',
|
||||
selectTradingStrategy: '选择交易策略',
|
||||
useStrategy: '使用策略',
|
||||
noStrategyManual: '-- 不使用策略(手动配置) --',
|
||||
active: ' (当前激活)',
|
||||
default: ' [默认]',
|
||||
noStrategyHint: '暂无策略,请先在策略工作室创建策略',
|
||||
strategyDetails: '策略详情',
|
||||
activating: '激活中',
|
||||
coinSource: '币种来源',
|
||||
marginLimit: '保证金上限',
|
||||
tradingParams: '交易参数',
|
||||
marginMode: '保证金模式',
|
||||
crossMargin: '全仓',
|
||||
isolatedMargin: '逐仓',
|
||||
competitionDisplay: '竞技场显示',
|
||||
show: '显示',
|
||||
hide: '隐藏',
|
||||
hiddenInCompetition: '隐藏后将不在竞技场页面显示此交易员',
|
||||
initialBalanceLabel: '初始余额 ($)',
|
||||
fetching: '获取中...',
|
||||
fetchCurrentBalance: '获取当前余额',
|
||||
balanceUpdateHint: '用于手动更新初始余额基准(例如充值/提现后)',
|
||||
autoFetchBalanceInfo: '系统将自动获取您的账户净值作为初始余额',
|
||||
fetchingBalance: '正在获取余额…',
|
||||
editTrader: '保存修改',
|
||||
createTraderButton: '创建交易员',
|
||||
|
||||
// AI Model Configuration
|
||||
officialAPI: '官方API',
|
||||
customAPI: '自定义API',
|
||||
@@ -1922,6 +1836,8 @@ export const translations = {
|
||||
passwordRequired: '请输入密码',
|
||||
invalidEmail: '邮箱格式不正确',
|
||||
passwordTooShort: '密码至少需要6个字符',
|
||||
showPassword: '显示密码',
|
||||
hidePassword: '隐藏密码',
|
||||
|
||||
// Landing Page
|
||||
features: '功能',
|
||||
|
||||
@@ -938,7 +938,35 @@ tr:hover {
|
||||
animation: holo-shift 8s ease infinite, holo-flicker 4s infinite;
|
||||
}
|
||||
|
||||
/* Holographic overlay effect */
|
||||
/* Mobile Breathing Animation */
|
||||
@keyframes holo-breath {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-breath-mobile {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.animate-breath-mobile {
|
||||
animation: holo-breath 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Optimize premium effects for mobile */
|
||||
.char-premium-effects {
|
||||
filter:
|
||||
drop-shadow(0 0 1px rgba(240, 185, 11, 0.6)) drop-shadow(0 0 1px rgba(0, 240, 255, 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
.holo-overlay {
|
||||
/* Complex gradient + noise for texture */
|
||||
background:
|
||||
@@ -951,3 +979,21 @@ tr:hover {
|
||||
mix-blend-mode: overlay;
|
||||
background-blend-mode: overlay, normal;
|
||||
}
|
||||
|
||||
/* Chromatic Aberration & Rim Light */
|
||||
.char-premium-effects {
|
||||
filter:
|
||||
/* Rim Light: Main Warm Gold + Sharp White Highlight (No Cyan to avoid green) */
|
||||
drop-shadow(0 0 2px rgba(240, 185, 11, 0.6)) drop-shadow(0 0 1px rgba(255, 255, 255, 0.4))
|
||||
/* Chromatic Aberration: Red/Blue shift (Subtle) */
|
||||
drop-shadow(-1px 0 0 rgba(255, 50, 50, 0.4)) drop-shadow(1px 0 0 rgba(50, 50, 255, 0.4));
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.char-premium-effects:hover {
|
||||
filter:
|
||||
/* Intense Gold Glow on Hover */
|
||||
drop-shadow(0 0 8px rgba(240, 185, 11, 0.8)) drop-shadow(0 0 2px rgba(255, 255, 255, 0.6))
|
||||
/* Enhanced Aberration */
|
||||
drop-shadow(-2px 0 0 rgba(255, 50, 50, 0.5)) drop-shadow(2px 0 0 rgba(50, 50, 255, 0.5));
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { DecisionCard } from '../components/DecisionCard'
|
||||
import { PositionHistory } from '../components/PositionHistory'
|
||||
import { PunkAvatar, getTraderAvatar } from '../components/PunkAvatar'
|
||||
import { confirmToast, notify } from '../lib/notify'
|
||||
import { formatPrice, formatQuantity } from '../utils/format'
|
||||
import { t, type Language } from '../i18n/translations'
|
||||
import { LogOut, Loader2, Eye, EyeOff, Copy, Check } from 'lucide-react'
|
||||
import { DeepVoidBackground } from '../components/DeepVoidBackground'
|
||||
@@ -654,9 +653,9 @@ export function TraderDashboardPage({
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.entry_price)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.mark_price)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{formatQuantity(pos.quantity)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{pos.entry_price.toFixed(4)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{pos.mark_price.toFixed(4)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{pos.quantity.toFixed(4)}</td>
|
||||
<td className="px-1 py-3 font-mono font-bold whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{(pos.quantity * pos.mark_price).toFixed(2)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-center text-nofx-gold hidden md:table-cell">{pos.leverage}x</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right">
|
||||
@@ -668,7 +667,7 @@ export function TraderDashboardPage({
|
||||
{pos.unrealized_pnl.toFixed(2)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-muted hidden md:table-cell">{formatPrice(pos.liquidation_price)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-muted hidden md:table-cell">{pos.liquidation_price.toFixed(4)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* 数字格式化工具
|
||||
*
|
||||
* formatPrice: 根据数值大小自适应显示精度,避免极小数显示为 0.0000
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化价格,根据数值大小自适应精度
|
||||
* 对于极小的数字(如 meme 币价格 0.000000166),会保留足够的有效数字
|
||||
*
|
||||
* @param price 价格数值
|
||||
* @param minDecimals 最少小数位数(默认 2)
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export function formatPrice(price: number | undefined | null, minDecimals = 2): string {
|
||||
if (price === undefined || price === null || isNaN(price)) {
|
||||
return '0'
|
||||
}
|
||||
|
||||
if (price === 0) {
|
||||
return '0'
|
||||
}
|
||||
|
||||
const absPrice = Math.abs(price)
|
||||
|
||||
// 根据价格大小决定显示精度
|
||||
let decimals: number
|
||||
if (absPrice < 0.000001) {
|
||||
// 极小价格 (如 CHEEMS, SHIB 等 meme 币)
|
||||
decimals = 15
|
||||
} else if (absPrice < 0.0001) {
|
||||
// 很小价格 (如 PEPE, FLOKI, BONK)
|
||||
decimals = 12
|
||||
} else if (absPrice < 0.01) {
|
||||
// 小价格
|
||||
decimals = 10
|
||||
} else if (absPrice < 1) {
|
||||
// 中等价格
|
||||
decimals = 8
|
||||
} else if (absPrice < 1000) {
|
||||
// 正常价格
|
||||
decimals = 4
|
||||
} else {
|
||||
// 大价格 (如 BTC)
|
||||
decimals = 2
|
||||
}
|
||||
|
||||
// 确保至少有 minDecimals 位小数
|
||||
decimals = Math.max(decimals, minDecimals)
|
||||
|
||||
// 格式化并去除尾部多余的零
|
||||
let formatted = price.toFixed(decimals)
|
||||
|
||||
// 去除尾部零(保留小数点后至少 minDecimals 位)
|
||||
if (formatted.includes('.')) {
|
||||
// 先去掉所有尾部零
|
||||
formatted = formatted.replace(/\.?0+$/, '')
|
||||
// 如果小数位不足 minDecimals,补零
|
||||
const dotIndex = formatted.indexOf('.')
|
||||
if (dotIndex === -1) {
|
||||
formatted += '.' + '0'.repeat(minDecimals)
|
||||
} else {
|
||||
const currentDecimals = formatted.length - dotIndex - 1
|
||||
if (currentDecimals < minDecimals) {
|
||||
formatted += '0'.repeat(minDecimals - currentDecimals)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数量,根据数值大小自适应精度
|
||||
*
|
||||
* @param quantity 数量
|
||||
* @param minDecimals 最少小数位数(默认 2)
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export function formatQuantity(quantity: number | undefined | null, minDecimals = 2): string {
|
||||
if (quantity === undefined || quantity === null || isNaN(quantity)) {
|
||||
return '0'
|
||||
}
|
||||
|
||||
if (quantity === 0) {
|
||||
return '0'
|
||||
}
|
||||
|
||||
const absQty = Math.abs(quantity)
|
||||
|
||||
let decimals: number
|
||||
if (absQty >= 1000000) {
|
||||
decimals = 0
|
||||
} else if (absQty >= 1000) {
|
||||
decimals = 2
|
||||
} else if (absQty >= 1) {
|
||||
decimals = 4
|
||||
} else {
|
||||
decimals = 8
|
||||
}
|
||||
|
||||
decimals = Math.max(decimals, minDecimals)
|
||||
|
||||
let formatted = quantity.toFixed(decimals)
|
||||
if (formatted.includes('.')) {
|
||||
formatted = formatted.replace(/\.?0+$/, '')
|
||||
const dotIndex = formatted.indexOf('.')
|
||||
if (dotIndex === -1) {
|
||||
formatted += '.' + '0'.repeat(minDecimals)
|
||||
} else {
|
||||
const currentDecimals = formatted.length - dotIndex - 1
|
||||
if (currentDecimals < minDecimals) {
|
||||
formatted += '0'.repeat(minDecimals - currentDecimals)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
*
|
||||
* @param value 百分比值
|
||||
* @param decimals 小数位数(默认 2)
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export function formatPercent(value: number | undefined | null, decimals = 2): string {
|
||||
if (value === undefined || value === null || isNaN(value)) {
|
||||
return '0.00'
|
||||
}
|
||||
return value.toFixed(decimals)
|
||||
}
|
||||
|
||||
export default { formatPrice, formatQuantity, formatPercent }
|
||||
Reference in New Issue
Block a user