2 Commits

Author SHA1 Message Date
google-labs-jules[bot]
f63eab72f7 🎨 Palette: Fix backend CI failures
- Added `//go:build ignore` to standalone scripts in `scripts/` to prevent redeclaration of `main`.
- Fixed redundant newline in `fmt.Println` in `cmd/lighter_test/main.go`.
- Updated `trader/hyperliquid/trader_test.go` to match the actual error message phrasing.

Co-authored-by: tinkle-community <240652709+tinkle-community@users.noreply.github.com>
2026-02-05 16:11:02 +00:00
google-labs-jules[bot]
6c26f9eb95 🎨 Palette: Improve LoginPage accessibility
- Added aria-label and aria-pressed to the password visibility toggle in LoginPage.
- Added showPassword and hidePassword translations to both English and Chinese.
- Ensured changes are micro and focused on accessibility.

Co-authored-by: tinkle-community <240652709+tinkle-community@users.noreply.github.com>
2026-02-05 15:54:28 +00:00
26 changed files with 261 additions and 659 deletions

3
.Jules/palette.md Normal file
View 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`.

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
//go:build ignore
package main
import (

View File

@@ -1,3 +1,5 @@
//go:build ignore
package main
import (

View File

@@ -1,3 +1,5 @@
//go:build ignore
package main
import (

View File

@@ -1,3 +1,5 @@
//go:build ignore
package main
import (

View File

@@ -1,3 +1,5 @@
//go:build ignore
package main
import (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: '功能',

View File

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

View File

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

View File

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