mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 09:24:36 +08:00
feat: add grid risk panel with API endpoint
- Task 13: Add GridRiskInfo type to frontend - Task 14: Add /traders/:id/grid-risk API endpoint - Task 15: Add GetGridRiskInfo method to AutoTrader - Task 16: Create GridRiskPanel component with i18n
This commit is contained in:
@@ -157,6 +157,7 @@ func (s *Server) setupRoutes() {
|
|||||||
protected.POST("/traders/:id/sync-balance", s.handleSyncBalance)
|
protected.POST("/traders/:id/sync-balance", s.handleSyncBalance)
|
||||||
protected.POST("/traders/:id/close-position", s.handleClosePosition)
|
protected.POST("/traders/:id/close-position", s.handleClosePosition)
|
||||||
protected.PUT("/traders/:id/competition", s.handleToggleCompetition)
|
protected.PUT("/traders/:id/competition", s.handleToggleCompetition)
|
||||||
|
protected.GET("/traders/:id/grid-risk", s.handleGetGridRiskInfo)
|
||||||
|
|
||||||
// AI model configuration
|
// AI model configuration
|
||||||
protected.GET("/models", s.handleGetModelConfigs)
|
protected.GET("/models", s.handleGetModelConfigs)
|
||||||
@@ -1096,6 +1097,20 @@ func (s *Server) handleToggleCompetition(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGetGridRiskInfo returns current risk information for a grid trader
|
||||||
|
func (s *Server) handleGetGridRiskInfo(c *gin.Context) {
|
||||||
|
traderID := c.Param("id")
|
||||||
|
|
||||||
|
autoTrader, err := s.traderManager.GetTrader(traderID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "trader not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
riskInfo := autoTrader.GetGridRiskInfo()
|
||||||
|
c.JSON(http.StatusOK, riskInfo)
|
||||||
|
}
|
||||||
|
|
||||||
// handleSyncBalance Sync exchange balance to initial_balance (Option B: Manual Sync + Option C: Smart Detection)
|
// handleSyncBalance Sync exchange balance to initial_balance (Option B: Manual Sync + Option C: Smart Detection)
|
||||||
func (s *Server) handleSyncBalance(c *gin.Context) {
|
func (s *Server) handleSyncBalance(c *gin.Context) {
|
||||||
userID := c.GetString("user_id")
|
userID := c.GetString("user_id")
|
||||||
@@ -1369,7 +1384,7 @@ func (s *Server) handleClosePosition(c *gin.Context) {
|
|||||||
|
|
||||||
if closeErr != nil {
|
if closeErr != nil {
|
||||||
logger.Infof("❌ Close position failed: symbol=%s, side=%s, error=%v", req.Symbol, req.Side, closeErr)
|
logger.Infof("❌ Close position failed: symbol=%s, side=%s, error=%v", req.Symbol, req.Side, closeErr)
|
||||||
SafeInternalError(c, "Failed to close position", closeErr)
|
SafeInternalError(c, "Close position", closeErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1705,8 +1720,15 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
|||||||
logger.Infof("🔓 Decrypted model config data (UserID: %s)", userID)
|
logger.Infof("🔓 Decrypted model config data (UserID: %s)", userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update each model's configuration
|
// Update each model's configuration and track traders that need reload
|
||||||
|
tradersToReload := make(map[string]bool)
|
||||||
for modelID, modelData := range req.Models {
|
for modelID, modelData := range req.Models {
|
||||||
|
// Find traders using this AI model BEFORE updating
|
||||||
|
traders, _ := s.store.Trader().ListByAIModelID(userID, modelID)
|
||||||
|
for _, t := range traders {
|
||||||
|
tradersToReload[t.ID] = true
|
||||||
|
}
|
||||||
|
|
||||||
err := s.store.AIModel().Update(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL, modelData.CustomModelName)
|
err := s.store.AIModel().Update(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL, modelData.CustomModelName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SafeInternalError(c, fmt.Sprintf("Update model %s", modelID), err)
|
SafeInternalError(c, fmt.Sprintf("Update model %s", modelID), err)
|
||||||
@@ -1714,6 +1736,12 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove affected traders from memory BEFORE reloading to pick up new config
|
||||||
|
for traderID := range tradersToReload {
|
||||||
|
logger.Infof("🔄 Removing trader %s from memory to reload with new AI model config", traderID)
|
||||||
|
s.traderManager.RemoveTrader(traderID)
|
||||||
|
}
|
||||||
|
|
||||||
// Reload all traders for this user to make new config take effect immediately
|
// Reload all traders for this user to make new config take effect immediately
|
||||||
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1825,8 +1853,15 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
|||||||
logger.Infof("🔓 Decrypted exchange config data (UserID: %s)", userID)
|
logger.Infof("🔓 Decrypted exchange config data (UserID: %s)", userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update each exchange's configuration
|
// Update each exchange's configuration and track traders that need reload
|
||||||
|
tradersToReload := make(map[string]bool)
|
||||||
for exchangeID, exchangeData := range req.Exchanges {
|
for exchangeID, exchangeData := range req.Exchanges {
|
||||||
|
// Find traders using this exchange BEFORE updating
|
||||||
|
traders, _ := s.store.Trader().ListByExchangeID(userID, exchangeID)
|
||||||
|
for _, t := range traders {
|
||||||
|
tradersToReload[t.ID] = true
|
||||||
|
}
|
||||||
|
|
||||||
err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey, exchangeData.LighterAPIKeyIndex)
|
err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey, exchangeData.LighterAPIKeyIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SafeInternalError(c, fmt.Sprintf("Update exchange %s", exchangeID), err)
|
SafeInternalError(c, fmt.Sprintf("Update exchange %s", exchangeID), err)
|
||||||
@@ -1834,6 +1869,12 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove affected traders from memory BEFORE reloading to pick up new config
|
||||||
|
for traderID := range tradersToReload {
|
||||||
|
logger.Infof("🔄 Removing trader %s from memory to reload with new exchange config", traderID)
|
||||||
|
s.traderManager.RemoveTrader(traderID)
|
||||||
|
}
|
||||||
|
|
||||||
// Reload all traders for this user to make new config take effect immediately
|
// Reload all traders for this user to make new config take effect immediately
|
||||||
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1345,6 +1345,150 @@ func (at *AutoTrader) initializeGridLevelsLocked(currentPrice float64, config *s
|
|||||||
at.gridState.Levels = levels
|
at.gridState.Levels = levels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GridRiskInfo contains risk information for frontend display
|
||||||
|
type GridRiskInfo struct {
|
||||||
|
CurrentLeverage int `json:"current_leverage"`
|
||||||
|
EffectiveLeverage float64 `json:"effective_leverage"`
|
||||||
|
RecommendedLeverage int `json:"recommended_leverage"`
|
||||||
|
|
||||||
|
CurrentPosition float64 `json:"current_position"`
|
||||||
|
MaxPosition float64 `json:"max_position"`
|
||||||
|
PositionPercent float64 `json:"position_percent"`
|
||||||
|
|
||||||
|
LiquidationPrice float64 `json:"liquidation_price"`
|
||||||
|
LiquidationDistance float64 `json:"liquidation_distance"`
|
||||||
|
|
||||||
|
RegimeLevel string `json:"regime_level"`
|
||||||
|
|
||||||
|
ShortBoxUpper float64 `json:"short_box_upper"`
|
||||||
|
ShortBoxLower float64 `json:"short_box_lower"`
|
||||||
|
MidBoxUpper float64 `json:"mid_box_upper"`
|
||||||
|
MidBoxLower float64 `json:"mid_box_lower"`
|
||||||
|
LongBoxUpper float64 `json:"long_box_upper"`
|
||||||
|
LongBoxLower float64 `json:"long_box_lower"`
|
||||||
|
CurrentPrice float64 `json:"current_price"`
|
||||||
|
|
||||||
|
BreakoutLevel string `json:"breakout_level"`
|
||||||
|
BreakoutDirection string `json:"breakout_direction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGridRiskInfo returns current risk information for frontend display
|
||||||
|
func (at *AutoTrader) GetGridRiskInfo() *GridRiskInfo {
|
||||||
|
gridConfig := at.config.StrategyConfig.GridConfig
|
||||||
|
if gridConfig == nil {
|
||||||
|
return &GridRiskInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
at.gridState.mu.RLock()
|
||||||
|
defer at.gridState.mu.RUnlock()
|
||||||
|
|
||||||
|
// Get current price
|
||||||
|
currentPrice, _ := at.trader.GetMarketPrice(gridConfig.Symbol)
|
||||||
|
|
||||||
|
// Calculate effective leverage
|
||||||
|
totalInvestment := gridConfig.TotalInvestment
|
||||||
|
leverage := gridConfig.Leverage
|
||||||
|
|
||||||
|
// Get current position value
|
||||||
|
positions, _ := at.trader.GetPositions()
|
||||||
|
var currentPositionValue float64
|
||||||
|
var currentPositionSize float64
|
||||||
|
for _, pos := range positions {
|
||||||
|
if sym, _ := pos["symbol"].(string); sym == gridConfig.Symbol {
|
||||||
|
size, _ := pos["positionAmt"].(float64)
|
||||||
|
entry, _ := pos["entryPrice"].(float64)
|
||||||
|
currentPositionValue = math.Abs(size * entry)
|
||||||
|
currentPositionSize = size
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveLeverage := 0.0
|
||||||
|
if totalInvestment > 0 {
|
||||||
|
effectiveLeverage = currentPositionValue / totalInvestment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate max position based on regime
|
||||||
|
regimeLevel := market.RegimeLevel(at.gridState.CurrentRegimeLevel)
|
||||||
|
if regimeLevel == "" {
|
||||||
|
regimeLevel = market.RegimeLevelStandard
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default position limit since GridStrategyConfig doesn't have regime-specific limits
|
||||||
|
// Default is 70% for standard regime
|
||||||
|
maxPositionPct := 70.0
|
||||||
|
switch regimeLevel {
|
||||||
|
case market.RegimeLevelNarrow:
|
||||||
|
maxPositionPct = 40.0
|
||||||
|
case market.RegimeLevelStandard:
|
||||||
|
maxPositionPct = 70.0
|
||||||
|
case market.RegimeLevelWide:
|
||||||
|
maxPositionPct = 60.0
|
||||||
|
case market.RegimeLevelVolatile:
|
||||||
|
maxPositionPct = 40.0
|
||||||
|
}
|
||||||
|
|
||||||
|
maxPosition := totalInvestment * maxPositionPct / 100 * float64(leverage)
|
||||||
|
|
||||||
|
// Use default leverage limits since GridStrategyConfig doesn't have regime-specific limits
|
||||||
|
recommendedLeverage := leverage
|
||||||
|
switch regimeLevel {
|
||||||
|
case market.RegimeLevelNarrow:
|
||||||
|
recommendedLeverage = min(leverage, 2)
|
||||||
|
case market.RegimeLevelStandard:
|
||||||
|
recommendedLeverage = min(leverage, 4)
|
||||||
|
case market.RegimeLevelWide:
|
||||||
|
recommendedLeverage = min(leverage, 3)
|
||||||
|
case market.RegimeLevelVolatile:
|
||||||
|
recommendedLeverage = min(leverage, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate liquidation distance
|
||||||
|
liquidationDistance := 100.0 / float64(leverage) * 0.9 // ~90% of theoretical max
|
||||||
|
|
||||||
|
var liquidationPrice float64
|
||||||
|
if currentPositionSize != 0 && currentPrice > 0 {
|
||||||
|
if currentPositionSize > 0 {
|
||||||
|
// Long position: liquidation below entry
|
||||||
|
liquidationPrice = currentPrice * (1 - liquidationDistance/100)
|
||||||
|
} else {
|
||||||
|
// Short position: liquidation above entry
|
||||||
|
liquidationPrice = currentPrice * (1 + liquidationDistance/100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
positionPercent := 0.0
|
||||||
|
if maxPosition > 0 {
|
||||||
|
positionPercent = currentPositionValue / maxPosition * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GridRiskInfo{
|
||||||
|
CurrentLeverage: leverage,
|
||||||
|
EffectiveLeverage: effectiveLeverage,
|
||||||
|
RecommendedLeverage: recommendedLeverage,
|
||||||
|
|
||||||
|
CurrentPosition: currentPositionValue,
|
||||||
|
MaxPosition: maxPosition,
|
||||||
|
PositionPercent: positionPercent,
|
||||||
|
|
||||||
|
LiquidationPrice: liquidationPrice,
|
||||||
|
LiquidationDistance: liquidationDistance,
|
||||||
|
|
||||||
|
RegimeLevel: string(regimeLevel),
|
||||||
|
|
||||||
|
ShortBoxUpper: at.gridState.ShortBoxUpper,
|
||||||
|
ShortBoxLower: at.gridState.ShortBoxLower,
|
||||||
|
MidBoxUpper: at.gridState.MidBoxUpper,
|
||||||
|
MidBoxLower: at.gridState.MidBoxLower,
|
||||||
|
LongBoxUpper: at.gridState.LongBoxUpper,
|
||||||
|
LongBoxLower: at.gridState.LongBoxLower,
|
||||||
|
CurrentPrice: currentPrice,
|
||||||
|
|
||||||
|
BreakoutLevel: at.gridState.BreakoutLevel,
|
||||||
|
BreakoutDirection: at.gridState.BreakoutDirection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// checkAndExecuteStopLoss checks if any filled level has exceeded stop loss and closes it
|
// checkAndExecuteStopLoss checks if any filled level has exceeded stop loss and closes it
|
||||||
func (at *AutoTrader) checkAndExecuteStopLoss() {
|
func (at *AutoTrader) checkAndExecuteStopLoss() {
|
||||||
gridConfig := at.config.StrategyConfig.GridConfig
|
gridConfig := at.config.StrategyConfig.GridConfig
|
||||||
|
|||||||
409
web/src/components/strategy/GridRiskPanel.tsx
Normal file
409
web/src/components/strategy/GridRiskPanel.tsx
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { Shield, TrendingUp, AlertTriangle, Activity, Box } from 'lucide-react'
|
||||||
|
import type { GridRiskInfo } from '../../types'
|
||||||
|
|
||||||
|
interface GridRiskPanelProps {
|
||||||
|
traderId: string
|
||||||
|
language?: string
|
||||||
|
refreshInterval?: number // ms, default 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GridRiskPanel({
|
||||||
|
traderId,
|
||||||
|
language = 'en',
|
||||||
|
refreshInterval = 5000,
|
||||||
|
}: GridRiskPanelProps) {
|
||||||
|
const [riskInfo, setRiskInfo] = useState<GridRiskInfo | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const t = (key: string) => {
|
||||||
|
const translations: Record<string, Record<string, string>> = {
|
||||||
|
// Section titles
|
||||||
|
leverageInfo: { zh: '杠杆信息', en: 'Leverage Info' },
|
||||||
|
positionInfo: { zh: '仓位信息', en: 'Position Info' },
|
||||||
|
liquidationInfo: { zh: '清算信息', en: 'Liquidation Info' },
|
||||||
|
marketState: { zh: '市场状态', en: 'Market State' },
|
||||||
|
boxState: { zh: '盒子状态', en: 'Box State' },
|
||||||
|
|
||||||
|
// Leverage
|
||||||
|
currentLeverage: { zh: '当前杠杆', en: 'Current Leverage' },
|
||||||
|
effectiveLeverage: { zh: '有效杠杆', en: 'Effective Leverage' },
|
||||||
|
recommendedLeverage: { zh: '建议杠杆', en: 'Recommended Leverage' },
|
||||||
|
|
||||||
|
// Position
|
||||||
|
currentPosition: { zh: '当前仓位', en: 'Current Position' },
|
||||||
|
maxPosition: { zh: '最大仓位', en: 'Max Position' },
|
||||||
|
positionPercent: { zh: '仓位占比', en: 'Position %' },
|
||||||
|
|
||||||
|
// Liquidation
|
||||||
|
liquidationPrice: { zh: '清算价格', en: 'Liquidation Price' },
|
||||||
|
liquidationDistance: { zh: '清算距离', en: 'Liquidation Distance' },
|
||||||
|
|
||||||
|
// Market
|
||||||
|
regimeLevel: { zh: '波动级别', en: 'Regime Level' },
|
||||||
|
currentPrice: { zh: '当前价格', en: 'Current Price' },
|
||||||
|
breakoutLevel: { zh: '突破级别', en: 'Breakout Level' },
|
||||||
|
breakoutDirection: { zh: '突破方向', en: 'Breakout Direction' },
|
||||||
|
|
||||||
|
// Box
|
||||||
|
shortBox: { zh: '短期盒子', en: 'Short Box' },
|
||||||
|
midBox: { zh: '中期盒子', en: 'Mid Box' },
|
||||||
|
longBox: { zh: '长期盒子', en: 'Long Box' },
|
||||||
|
|
||||||
|
// Regime levels
|
||||||
|
narrow: { zh: '窄幅震荡', en: 'Narrow' },
|
||||||
|
standard: { zh: '标准震荡', en: 'Standard' },
|
||||||
|
wide: { zh: '宽幅震荡', en: 'Wide' },
|
||||||
|
volatile: { zh: '剧烈震荡', en: 'Volatile' },
|
||||||
|
trending: { zh: '趋势', en: 'Trending' },
|
||||||
|
|
||||||
|
// Breakout levels
|
||||||
|
none: { zh: '无', en: 'None' },
|
||||||
|
short: { zh: '短期', en: 'Short' },
|
||||||
|
mid: { zh: '中期', en: 'Mid' },
|
||||||
|
long: { zh: '长期', en: 'Long' },
|
||||||
|
|
||||||
|
// Directions
|
||||||
|
up: { zh: '向上', en: 'Up' },
|
||||||
|
down: { zh: '向下', en: 'Down' },
|
||||||
|
|
||||||
|
// Status
|
||||||
|
loading: { zh: '加载中...', en: 'Loading...' },
|
||||||
|
error: { zh: '加载失败', en: 'Load Failed' },
|
||||||
|
noData: { zh: '暂无数据', en: 'No Data' },
|
||||||
|
}
|
||||||
|
return translations[key]?.[language] || key
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRiskInfo = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await fetch(`/api/traders/${traderId}/grid-risk`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
setRiskInfo(data)
|
||||||
|
setError(null)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [traderId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRiskInfo()
|
||||||
|
const interval = setInterval(fetchRiskInfo, refreshInterval)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [fetchRiskInfo, refreshInterval])
|
||||||
|
|
||||||
|
const getRegimeColor = (regime: string) => {
|
||||||
|
switch (regime) {
|
||||||
|
case 'narrow':
|
||||||
|
return '#0ECB81' // Green - safe
|
||||||
|
case 'standard':
|
||||||
|
return '#F0B90B' // Yellow - normal
|
||||||
|
case 'wide':
|
||||||
|
return '#F7931A' // Orange - caution
|
||||||
|
case 'volatile':
|
||||||
|
return '#F6465D' // Red - danger
|
||||||
|
case 'trending':
|
||||||
|
return '#8B5CF6' // Purple - trending
|
||||||
|
default:
|
||||||
|
return '#848E9C' // Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBreakoutColor = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case 'none':
|
||||||
|
return '#0ECB81' // Green - safe
|
||||||
|
case 'short':
|
||||||
|
return '#F0B90B' // Yellow - minor
|
||||||
|
case 'mid':
|
||||||
|
return '#F7931A' // Orange - warning
|
||||||
|
case 'long':
|
||||||
|
return '#F6465D' // Red - critical
|
||||||
|
default:
|
||||||
|
return '#848E9C'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPositionColor = (percent: number) => {
|
||||||
|
if (percent < 50) return '#0ECB81' // Green
|
||||||
|
if (percent < 80) return '#F0B90B' // Yellow
|
||||||
|
return '#F6465D' // Red
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatPrice = (price: number) => {
|
||||||
|
if (price === 0) return '-'
|
||||||
|
if (price >= 1000) return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
|
if (price >= 1) return price.toFixed(4)
|
||||||
|
return price.toFixed(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatUSD = (value: number) => {
|
||||||
|
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionStyle = {
|
||||||
|
background: '#0B0E11',
|
||||||
|
border: '1px solid #2B3139',
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelStyle = { color: '#848E9C' }
|
||||||
|
const valueStyle = { color: '#EAECEF' }
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center" style={{ color: '#848E9C' }}>
|
||||||
|
{t('loading')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center" style={{ color: '#F6465D' }}>
|
||||||
|
{t('error')}: {error}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!riskInfo) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center" style={{ color: '#848E9C' }}>
|
||||||
|
{t('noData')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Leverage Info */}
|
||||||
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<TrendingUp className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
|
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('leverageInfo')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('currentLeverage')}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-mono" style={valueStyle}>
|
||||||
|
{riskInfo.current_leverage}x
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('effectiveLeverage')}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-mono" style={{ color: '#F0B90B' }}>
|
||||||
|
{riskInfo.effective_leverage.toFixed(2)}x
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('recommendedLeverage')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-lg font-mono"
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
riskInfo.current_leverage > riskInfo.recommended_leverage
|
||||||
|
? '#F6465D'
|
||||||
|
: '#0ECB81',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{riskInfo.recommended_leverage}x
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Position Info */}
|
||||||
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Activity className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
|
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('positionInfo')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('currentPosition')}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-mono" style={valueStyle}>
|
||||||
|
{formatUSD(riskInfo.current_position)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('maxPosition')}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-mono" style={valueStyle}>
|
||||||
|
{formatUSD(riskInfo.max_position)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('positionPercent')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-lg font-mono"
|
||||||
|
style={{ color: getPositionColor(riskInfo.position_percent) }}
|
||||||
|
>
|
||||||
|
{riskInfo.position_percent.toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Position Progress Bar */}
|
||||||
|
<div className="h-2 rounded-full overflow-hidden" style={{ background: '#1E2329' }}>
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(riskInfo.position_percent, 100)}%`,
|
||||||
|
background: getPositionColor(riskInfo.position_percent),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liquidation Info */}
|
||||||
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<AlertTriangle className="w-4 h-4" style={{ color: '#F6465D' }} />
|
||||||
|
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('liquidationInfo')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('liquidationPrice')}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-mono" style={{ color: '#F6465D' }}>
|
||||||
|
{riskInfo.liquidation_price > 0 ? formatPrice(riskInfo.liquidation_price) : '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('liquidationDistance')}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-mono" style={{ color: '#F6465D' }}>
|
||||||
|
{riskInfo.liquidation_distance.toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Market State */}
|
||||||
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Shield className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
|
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('marketState')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('regimeLevel')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-lg font-medium"
|
||||||
|
style={{ color: getRegimeColor(riskInfo.regime_level) }}
|
||||||
|
>
|
||||||
|
{t(riskInfo.regime_level || 'standard')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('currentPrice')}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-mono" style={valueStyle}>
|
||||||
|
{formatPrice(riskInfo.current_price)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('breakoutLevel')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-lg font-medium"
|
||||||
|
style={{ color: getBreakoutColor(riskInfo.breakout_level) }}
|
||||||
|
>
|
||||||
|
{t(riskInfo.breakout_level || 'none')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs" style={labelStyle}>
|
||||||
|
{t('breakoutDirection')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-lg font-medium"
|
||||||
|
style={{
|
||||||
|
color: riskInfo.breakout_direction === 'up' ? '#0ECB81' : riskInfo.breakout_direction === 'down' ? '#F6465D' : '#848E9C',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{riskInfo.breakout_direction ? t(riskInfo.breakout_direction) : '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Box State */}
|
||||||
|
<div className="p-4 rounded-lg" style={sectionStyle}>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Box className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||||
|
<span className="font-medium text-sm" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('boxState')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Short Box */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs" style={labelStyle}>
|
||||||
|
{t('shortBox')}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-mono" style={valueStyle}>
|
||||||
|
{formatPrice(riskInfo.short_box_lower)} - {formatPrice(riskInfo.short_box_upper)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Mid Box */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs" style={labelStyle}>
|
||||||
|
{t('midBox')}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-mono" style={valueStyle}>
|
||||||
|
{formatPrice(riskInfo.mid_box_lower)} - {formatPrice(riskInfo.mid_box_upper)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Long Box */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs" style={labelStyle}>
|
||||||
|
{t('longBox')}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-mono" style={valueStyle}>
|
||||||
|
{formatPrice(riskInfo.long_box_lower)} - {formatPrice(riskInfo.long_box_upper)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -786,3 +786,36 @@ export interface PositionHistoryResponse {
|
|||||||
symbol_stats: SymbolStats[];
|
symbol_stats: SymbolStats[];
|
||||||
direction_stats: DirectionStats[];
|
direction_stats: DirectionStats[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grid Risk Information for frontend display
|
||||||
|
export interface GridRiskInfo {
|
||||||
|
// Leverage info
|
||||||
|
current_leverage: number
|
||||||
|
effective_leverage: number
|
||||||
|
recommended_leverage: number
|
||||||
|
|
||||||
|
// Position info
|
||||||
|
current_position: number
|
||||||
|
max_position: number
|
||||||
|
position_percent: number
|
||||||
|
|
||||||
|
// Liquidation info
|
||||||
|
liquidation_price: number
|
||||||
|
liquidation_distance: number
|
||||||
|
|
||||||
|
// Market state
|
||||||
|
regime_level: string
|
||||||
|
|
||||||
|
// Box state
|
||||||
|
short_box_upper: number
|
||||||
|
short_box_lower: number
|
||||||
|
mid_box_upper: number
|
||||||
|
mid_box_lower: number
|
||||||
|
long_box_upper: number
|
||||||
|
long_box_lower: number
|
||||||
|
current_price: number
|
||||||
|
|
||||||
|
// Breakout state
|
||||||
|
breakout_level: string
|
||||||
|
breakout_direction: string
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user