mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +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/close-position", s.handleClosePosition)
|
||||
protected.PUT("/traders/:id/competition", s.handleToggleCompetition)
|
||||
protected.GET("/traders/:id/grid-risk", s.handleGetGridRiskInfo)
|
||||
|
||||
// AI model configuration
|
||||
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)
|
||||
func (s *Server) handleSyncBalance(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
@@ -1369,7 +1384,7 @@ func (s *Server) handleClosePosition(c *gin.Context) {
|
||||
|
||||
if closeErr != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1705,8 +1720,15 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
||||
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 {
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
||||
if err != nil {
|
||||
@@ -1825,8 +1853,15 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
||||
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 {
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
||||
if err != nil {
|
||||
|
||||
@@ -1345,6 +1345,150 @@ func (at *AutoTrader) initializeGridLevelsLocked(currentPrice float64, config *s
|
||||
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
|
||||
func (at *AutoTrader) checkAndExecuteStopLoss() {
|
||||
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[];
|
||||
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