auto_trade_sys/recommendations-viewer/src/components/RecommendationsViewer.jsx
薇薇安 c7444d884c a
2026-01-15 23:06:21 +08:00

352 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { api } from '../services/api';
import './RecommendationsViewer.css';
function RecommendationsViewer() {
const [recommendations, setRecommendations] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [directionFilter, setDirectionFilter] = useState('');
const [showDetails, setShowDetails] = useState({});
useEffect(() => {
loadRecommendations();
// 每10秒静默更新价格不触发loading状态
const interval = setInterval(async () => {
try {
const result = await api.getRecommendations({
type: 'realtime',
direction: directionFilter,
limit: 50,
min_signal_strength: 5
});
const newData = result.data || [];
// 使用setState直接更新不触发loading状态
setRecommendations(prevRecommendations => {
if (newData.length === 0) {
return prevRecommendations;
}
// 实时推荐没有id使用symbol作为key
const newDataMap = new Map(newData.map(rec => [rec.symbol, rec]));
const prevMap = new Map(prevRecommendations.map(rec => [rec.symbol || rec.id, rec]));
// 合并数据:优先使用新数据(包含实时价格更新)
const updated = prevRecommendations.map(prevRec => {
const key = prevRec.symbol || prevRec.id;
const newRec = newDataMap.get(key);
if (newRec) {
return newRec;
}
return prevRec;
});
// 添加新出现的推荐
const newItems = newData.filter(newRec => !prevMap.has(newRec.symbol));
// 合并并去重按symbol
const merged = [...updated, ...newItems];
const uniqueMap = new Map();
merged.forEach(rec => {
const key = rec.symbol || rec.id;
if (!uniqueMap.has(key)) {
uniqueMap.set(key, rec);
}
});
return Array.from(uniqueMap.values());
});
} catch (err) {
// 静默失败,不显示错误
console.debug('静默更新价格失败:', err);
}
}, 10000); // 每10秒刷新
return () => {
clearInterval(interval);
};
}, [directionFilter]);
const loadRecommendations = async () => {
try {
setLoading(true);
setError(null);
const params = {
type: 'realtime',
limit: 50,
min_signal_strength: 5
};
if (directionFilter) {
params.direction = directionFilter;
}
const result = await api.getRecommendations(params);
const data = result.data || [];
setRecommendations(data);
} catch (err) {
setError(err.message);
console.error('加载推荐失败:', err);
} finally {
setLoading(false);
}
};
const toggleDetails = (key) => {
setShowDetails(prev => ({
...prev,
[key]: !prev[key]
}));
};
const formatTime = (timeStr) => {
if (!timeStr) return '-';
try {
const date = new Date(timeStr);
if (isNaN(date.getTime())) return timeStr;
// 使用北京时间格式化
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: 'Asia/Shanghai' // 明确使用北京时间
});
} catch (e) {
return timeStr;
}
};
const getSignalStrengthColor = (strength) => {
if (strength >= 8) return 'signal-strong';
if (strength >= 6) return 'signal-medium';
return 'signal-weak';
};
return (
<div className="recommendations-viewer">
<div className="viewer-header">
<h1>交易推荐</h1>
<div className="header-info">
<span className="update-info">每10秒自动更新</span>
<button
className="btn-refresh"
onClick={loadRecommendations}
disabled={loading}
>
{loading ? '刷新中...' : '手动刷新'}
</button>
</div>
</div>
<div className="filters">
<select
value={directionFilter}
onChange={(e) => setDirectionFilter(e.target.value)}
className="filter-select"
>
<option value="">全部方向</option>
<option value="BUY">做多</option>
<option value="SELL">做空</option>
</select>
</div>
{error && (
<div className="error-message">
{error}
</div>
)}
{loading ? (
<div className="loading">加载中...</div>
) : recommendations.length === 0 ? (
<div className="empty-state">
<p>暂无推荐记录</p>
</div>
) : (
<div className="recommendations-list">
{recommendations.map((rec) => {
const key = rec.id || rec.symbol;
return (
<div key={key} className="recommendation-card">
<div className="card-header">
<div className="card-title">
<span className="symbol">{rec.symbol}</span>
<span className={`direction ${rec.direction.toLowerCase()}`}>
{rec.direction === 'BUY' ? '做多' : '做空'}
</span>
</div>
<div className="card-meta">
<span className={`signal-strength ${getSignalStrengthColor(rec.signal_strength)}`}>
信号强度: {rec.signal_strength}/10
</span>
{rec.recommendation_time && (
<span className="time">{formatTime(rec.recommendation_time)}</span>
)}
</div>
</div>
<div className="card-content">
<div className="price-info">
<div className="price-item">
<label>当前价格:</label>
<span>
{parseFloat(rec.current_price || 0).toFixed(4)} USDT
{rec.price_updated && (
<span className="price-updated-badge" title="价格已通过WebSocket实时更新">🟢</span>
)}
</span>
</div>
{rec.change_percent !== undefined && rec.change_percent !== null && (
<div className="price-item">
<label>24h涨跌:</label>
<span className={rec.change_percent >= 0 ? 'positive' : 'negative'}>
{rec.change_percent >= 0 ? '+' : ''}{parseFloat(rec.change_percent).toFixed(2)}%
</span>
</div>
)}
</div>
<div className="recommendation-reason">
<strong>推荐原因:</strong>
<p>{rec.recommendation_reason || '-'}</p>
</div>
<div className="suggested-params">
<div className="param-item order-type">
<label>订单类型:</label>
<span className={`order-type-badge ${(rec.order_type || 'LIMIT').toLowerCase()}`}>
{rec.order_type === 'LIMIT' ? '限价单' : '市价单'}
</span>
</div>
{rec.order_type === 'LIMIT' && rec.suggested_limit_price && (
<div className="param-item limit-price">
<label>建议挂单价:</label>
<span className="limit-price-value">
{parseFloat(rec.suggested_limit_price || 0).toFixed(4)} USDT
{rec.current_price && (
<span className="price-diff">
({rec.direction === 'BUY' ? '低于' : '高于'}当前价
{Math.abs(((rec.suggested_limit_price - rec.current_price) / rec.current_price) * 100).toFixed(2)}%)
</span>
)}
</span>
</div>
)}
<div className="param-item">
<label>建议止损:</label>
<span>{parseFloat(rec.suggested_stop_loss || 0).toFixed(4)}</span>
</div>
<div className="param-item">
<label>第一目标:</label>
<span>{parseFloat(rec.suggested_take_profit_1 || 0).toFixed(4)}</span>
</div>
<div className="param-item">
<label>第二目标:</label>
<span>{parseFloat(rec.suggested_take_profit_2 || 0).toFixed(4)}</span>
</div>
<div className="param-item">
<label>建议仓位:</label>
<span>{(parseFloat(rec.suggested_position_percent || 0) * 100).toFixed(2)}%</span>
</div>
<div className="param-item">
<label>建议杠杆:</label>
<span>{rec.suggested_leverage || 10}x</span>
</div>
</div>
<button
className="btn-toggle-details"
onClick={() => toggleDetails(key)}
>
{showDetails[key] ? '隐藏' : '显示'}详细信息
</button>
{showDetails[key] && (
<div className="details-panel">
<div className="details-section">
<h4>技术指标</h4>
<div className="indicators-grid">
{rec.rsi && (
<div className="indicator-item">
<label>RSI:</label>
<span>{parseFloat(rec.rsi).toFixed(2)}</span>
</div>
)}
{rec.macd_histogram !== null && rec.macd_histogram !== undefined && (
<div className="indicator-item">
<label>MACD:</label>
<span>{parseFloat(rec.macd_histogram).toFixed(6)}</span>
</div>
)}
{rec.ema20 && (
<div className="indicator-item">
<label>EMA20:</label>
<span>{parseFloat(rec.ema20).toFixed(4)}</span>
</div>
)}
{rec.ema50 && (
<div className="indicator-item">
<label>EMA50:</label>
<span>{parseFloat(rec.ema50).toFixed(4)}</span>
</div>
)}
{rec.atr && (
<div className="indicator-item">
<label>ATR:</label>
<span>{parseFloat(rec.atr).toFixed(4)}</span>
</div>
)}
</div>
</div>
{rec.market_regime && (
<div className="details-section">
<h4>市场状态</h4>
<p>{rec.market_regime === 'trending' ? '趋势市场' : '震荡市场'}</p>
</div>
)}
{rec.trend_4h && (
<div className="details-section">
<h4>4H趋势</h4>
<p>
{rec.trend_4h === 'up' ? '上涨' :
rec.trend_4h === 'down' ? '下跌' : '中性'}
</p>
</div>
)}
{rec.volume_24h && (
<div className="details-section">
<h4>24小时成交量</h4>
<p>{parseFloat(rec.volume_24h).toFixed(2)}</p>
</div>
)}
{rec.volatility && (
<div className="details-section">
<h4>波动率</h4>
<p>{parseFloat(rec.volatility).toFixed(4)}</p>
</div>
)}
</div>
)}
</div>
</div>
);
})}
</div>
)}
</div>
);
}
export default RecommendationsViewer;