diff --git a/recommendations-viewer/.gitignore b/recommendations-viewer/.gitignore new file mode 100644 index 0000000..27b3e38 --- /dev/null +++ b/recommendations-viewer/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Environment variables +.env +.env.local +.env.production.local +.env.development.local diff --git a/recommendations-viewer/README.md b/recommendations-viewer/README.md new file mode 100644 index 0000000..60a2c6d --- /dev/null +++ b/recommendations-viewer/README.md @@ -0,0 +1,79 @@ +# 交易推荐查看器 + +一个独立的React前端应用,用于查看实时交易推荐。 + +## 功能特点 + +- ✅ 实时查看交易推荐(基于当前行情数据) +- ✅ 每10秒自动刷新价格 +- ✅ 支持按方向过滤(做多/做空) +- ✅ 显示详细的推荐信息(价格、止损、止盈、仓位、杠杆等) +- ✅ 显示技术指标详情 +- ✅ 响应式设计,支持移动端 + +## 安装和运行 + +### 1. 安装依赖 + +```bash +cd recommendations-viewer +npm install +``` + +### 2. 配置API地址(可选) + +如果需要修改API地址,可以: + +- 在项目根目录创建 `.env` 文件: + ``` + VITE_API_URL=http://your-api-url.com + ``` + +- 或者修改 `vite.config.js` 中的代理配置 + +### 3. 启动开发服务器 + +```bash +npm run dev +``` + +应用将在 `http://localhost:3001` 启动 + +### 4. 构建生产版本 + +```bash +npm run build +``` + +构建后的文件在 `dist` 目录中 + +## 项目结构 + +``` +recommendations-viewer/ +├── src/ +│ ├── components/ +│ │ ├── RecommendationsViewer.jsx # 主组件 +│ │ └── RecommendationsViewer.css # 样式文件 +│ ├── services/ +│ │ └── api.js # API服务 +│ ├── main.jsx # 应用入口 +│ └── index.css # 全局样式 +├── index.html # HTML模板 +├── package.json # 项目配置 +├── vite.config.js # Vite配置 +└── README.md # 说明文档 +``` + +## API接口 + +应用使用以下API接口: + +- `GET /api/recommendations?type=realtime&direction=BUY&limit=50&min_signal_strength=5` + - 获取实时推荐列表 + +## 注意事项 + +- 这是一个只读应用,不包含任何操作功能(如标记、执行、取消等) +- 推荐数据每10秒自动刷新,确保价格信息实时更新 +- 如果API服务不可用,会显示错误信息 diff --git a/recommendations-viewer/index.html b/recommendations-viewer/index.html new file mode 100644 index 0000000..a2cdeb6 --- /dev/null +++ b/recommendations-viewer/index.html @@ -0,0 +1,12 @@ + + + + + + 交易推荐查看 + + +
+ + + diff --git a/recommendations-viewer/package.json b/recommendations-viewer/package.json new file mode 100644 index 0000000..2901f13 --- /dev/null +++ b/recommendations-viewer/package.json @@ -0,0 +1,18 @@ +{ + "name": "recommendations-viewer", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0" + } +} diff --git a/recommendations-viewer/src/components/RecommendationsViewer.css b/recommendations-viewer/src/components/RecommendationsViewer.css new file mode 100644 index 0000000..9b5ce65 --- /dev/null +++ b/recommendations-viewer/src/components/RecommendationsViewer.css @@ -0,0 +1,412 @@ +.recommendations-viewer { + padding: 20px; + max-width: 1400px; + margin: 0 auto; + min-height: 100vh; + background-color: #f5f5f5; +} + +.viewer-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 20px; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.viewer-header h1 { + margin: 0; + color: #333; + font-size: 24px; +} + +.header-info { + display: flex; + align-items: center; + gap: 15px; +} + +.update-info { + font-size: 12px; + color: #666; +} + +.btn-refresh { + padding: 8px 16px; + background-color: #2196F3; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.btn-refresh:hover:not(:disabled) { + background-color: #0b7dda; +} + +.btn-refresh:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +.filters { + display: flex; + gap: 10px; + margin-bottom: 20px; + padding: 15px; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.filter-select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + background-color: white; + cursor: pointer; +} + +.error-message { + padding: 12px; + background-color: #ffebee; + color: #c62828; + border-radius: 4px; + margin-bottom: 20px; +} + +.loading { + text-align: center; + padding: 40px; + color: #666; + background-color: white; + border-radius: 8px; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #666; + background-color: white; + border-radius: 8px; +} + +.empty-state p { + margin-bottom: 20px; + font-size: 16px; +} + +.recommendations-list { + display: grid; + gap: 20px; +} + +.recommendation-card { + background-color: white; + border-radius: 8px; + padding: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.3s; +} + +.recommendation-card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.card-title { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.symbol { + font-size: 18px; + font-weight: bold; + color: #333; +} + +.direction { + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: bold; +} + +.direction.buy { + background-color: #4CAF50; + color: white; +} + +.direction.sell { + background-color: #f44336; + color: white; +} + +.card-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 5px; +} + +.signal-strength { + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: bold; +} + +.signal-strong { + background-color: #4CAF50; + color: white; +} + +.signal-medium { + background-color: #FF9800; + color: white; +} + +.signal-weak { + background-color: #9E9E9E; + color: white; +} + +.time { + font-size: 12px; + color: #666; +} + +.card-content { + display: flex; + flex-direction: column; + gap: 15px; +} + +.price-info { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.price-item { + display: flex; + align-items: center; + gap: 8px; +} + +.price-item label { + font-weight: bold; + color: #666; +} + +.price-item .positive { + color: #4CAF50; +} + +.price-item .negative { + color: #f44336; +} + +.price-updated-badge { + margin-left: 5px; + font-size: 10px; +} + +.recommendation-reason { + background-color: #f5f5f5; + padding: 12px; + border-radius: 4px; +} + +.recommendation-reason strong { + display: block; + margin-bottom: 8px; + color: #333; +} + +.recommendation-reason p { + margin: 0; + color: #666; + line-height: 1.5; +} + +.suggested-params { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + padding: 15px; + background-color: #f9f9f9; + border-radius: 4px; +} + +.param-item { + display: flex; + flex-direction: column; + gap: 5px; +} + +.param-item label { + font-size: 12px; + color: #666; + font-weight: bold; +} + +.param-item span { + font-size: 14px; + color: #333; + font-weight: 500; +} + +.param-item.order-type { + grid-column: span 2; +} + +.order-type-badge { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: bold; +} + +.order-type-badge.limit { + background-color: #2196F3; + color: white; +} + +.order-type-badge.market { + background-color: #FF9800; + color: white; +} + +.param-item.limit-price { + grid-column: span 2; +} + +.limit-price-value { + display: flex; + flex-direction: column; + gap: 4px; +} + +.price-diff { + font-size: 11px; + color: #666; + font-weight: normal; + font-style: italic; +} + +.btn-toggle-details { + padding: 8px 16px; + background-color: #e0e0e0; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; + align-self: flex-start; +} + +.btn-toggle-details:hover { + background-color: #d0d0d0; +} + +.details-panel { + margin-top: 15px; + padding: 15px; + background-color: #f9f9f9; + border-radius: 4px; + border: 1px solid #e0e0e0; +} + +.details-section { + margin-bottom: 20px; +} + +.details-section:last-child { + margin-bottom: 0; +} + +.details-section h4 { + margin: 0 0 10px 0; + color: #333; + font-size: 14px; +} + +.indicators-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 10px; +} + +.indicator-item { + display: flex; + justify-content: space-between; + padding: 8px; + background-color: white; + border-radius: 4px; +} + +.indicator-item label { + font-size: 12px; + color: #666; +} + +.indicator-item span { + font-size: 12px; + color: #333; + font-weight: 500; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .recommendations-viewer { + padding: 10px; + } + + .viewer-header { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } + + .header-info { + width: 100%; + justify-content: space-between; + } + + .card-header { + flex-direction: column; + align-items: flex-start; + } + + .card-meta { + align-items: flex-start; + margin-top: 10px; + } + + .suggested-params { + grid-template-columns: 1fr; + } + + .param-item.order-type, + .param-item.limit-price { + grid-column: span 1; + } +} diff --git a/recommendations-viewer/src/components/RecommendationsViewer.jsx b/recommendations-viewer/src/components/RecommendationsViewer.jsx new file mode 100644 index 0000000..433a690 --- /dev/null +++ b/recommendations-viewer/src/components/RecommendationsViewer.jsx @@ -0,0 +1,349 @@ +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); + 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 ( +
+
+

交易推荐

+
+ 每10秒自动更新 + +
+
+ +
+ +
+ + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
加载中...
+ ) : recommendations.length === 0 ? ( +
+

暂无推荐记录

+
+ ) : ( +
+ {recommendations.map((rec) => { + const key = rec.id || rec.symbol; + return ( +
+
+
+ {rec.symbol} + + {rec.direction === 'BUY' ? '做多' : '做空'} + +
+
+ + 信号强度: {rec.signal_strength}/10 + + {rec.recommendation_time && ( + {formatTime(rec.recommendation_time)} + )} +
+
+ +
+
+
+ + + {parseFloat(rec.current_price || 0).toFixed(4)} USDT + {rec.price_updated && ( + 🟢 + )} + +
+ {rec.change_percent !== undefined && rec.change_percent !== null && ( +
+ + = 0 ? 'positive' : 'negative'}> + {rec.change_percent >= 0 ? '+' : ''}{parseFloat(rec.change_percent).toFixed(2)}% + +
+ )} +
+ +
+ 推荐原因: +

{rec.recommendation_reason || '-'}

+
+ +
+
+ + + {rec.order_type === 'LIMIT' ? '限价单' : '市价单'} + +
+ {rec.order_type === 'LIMIT' && rec.suggested_limit_price && ( +
+ + + {parseFloat(rec.suggested_limit_price || 0).toFixed(4)} USDT + {rec.current_price && ( + + ({rec.direction === 'BUY' ? '低于' : '高于'}当前价 + {Math.abs(((rec.suggested_limit_price - rec.current_price) / rec.current_price) * 100).toFixed(2)}%) + + )} + +
+ )} +
+ + {parseFloat(rec.suggested_stop_loss || 0).toFixed(4)} +
+
+ + {parseFloat(rec.suggested_take_profit_1 || 0).toFixed(4)} +
+
+ + {parseFloat(rec.suggested_take_profit_2 || 0).toFixed(4)} +
+
+ + {(parseFloat(rec.suggested_position_percent || 0) * 100).toFixed(2)}% +
+
+ + {rec.suggested_leverage || 10}x +
+
+ + + + {showDetails[key] && ( +
+
+

技术指标

+
+ {rec.rsi && ( +
+ + {parseFloat(rec.rsi).toFixed(2)} +
+ )} + {rec.macd_histogram !== null && rec.macd_histogram !== undefined && ( +
+ + {parseFloat(rec.macd_histogram).toFixed(6)} +
+ )} + {rec.ema20 && ( +
+ + {parseFloat(rec.ema20).toFixed(4)} +
+ )} + {rec.ema50 && ( +
+ + {parseFloat(rec.ema50).toFixed(4)} +
+ )} + {rec.atr && ( +
+ + {parseFloat(rec.atr).toFixed(4)} +
+ )} +
+
+ + {rec.market_regime && ( +
+

市场状态

+

{rec.market_regime === 'trending' ? '趋势市场' : '震荡市场'}

+
+ )} + + {rec.trend_4h && ( +
+

4H趋势

+

+ {rec.trend_4h === 'up' ? '上涨' : + rec.trend_4h === 'down' ? '下跌' : '中性'} +

+
+ )} + + {rec.volume_24h && ( +
+

24小时成交量

+

{parseFloat(rec.volume_24h).toFixed(2)}

+
+ )} + + {rec.volatility && ( +
+

波动率

+

{parseFloat(rec.volatility).toFixed(4)}

+
+ )} +
+ )} +
+
+ ); + })} +
+ )} +
+ ); +} + +export default RecommendationsViewer; diff --git a/recommendations-viewer/src/index.css b/recommendations-viewer/src/index.css new file mode 100644 index 0000000..2828298 --- /dev/null +++ b/recommendations-viewer/src/index.css @@ -0,0 +1,24 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: #f5f5f5; + color: #333; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +#root { + min-height: 100vh; +} diff --git a/recommendations-viewer/src/main.jsx b/recommendations-viewer/src/main.jsx new file mode 100644 index 0000000..06ae777 --- /dev/null +++ b/recommendations-viewer/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import RecommendationsViewer from './components/RecommendationsViewer' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/recommendations-viewer/src/services/api.js b/recommendations-viewer/src/services/api.js new file mode 100644 index 0000000..3ebba54 --- /dev/null +++ b/recommendations-viewer/src/services/api.js @@ -0,0 +1,26 @@ +// API服务 - 只包含推荐查询功能 +const API_BASE_URL = import.meta.env.VITE_API_URL || (import.meta.env.DEV ? '' : 'http://localhost:8000'); + +const buildUrl = (path) => { + const baseUrl = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; + const cleanPath = path.startsWith('/') ? path : `/${path}`; + return baseUrl ? `${baseUrl}${cleanPath}` : cleanPath; +}; + +export const api = { + // 获取实时推荐 + getRecommendations: async (params = {}) => { + // 默认使用实时推荐 + if (!params.type) { + params.type = 'realtime'; + } + const query = new URLSearchParams(params).toString(); + const url = query ? `${buildUrl('/api/recommendations')}?${query}` : buildUrl('/api/recommendations'); + const response = await fetch(url); + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: '获取推荐失败' })); + throw new Error(error.detail || '获取推荐失败'); + } + return response.json(); + } +}; diff --git a/recommendations-viewer/vite.config.js b/recommendations-viewer/vite.config.js new file mode 100644 index 0000000..b1f0263 --- /dev/null +++ b/recommendations-viewer/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3001, + proxy: { + '/api': { + target: 'http://asapi.deepx1.com', + changeOrigin: true + } + } + } +})