This commit is contained in:
薇薇安 2026-01-19 14:02:53 +08:00
parent 63687e9526
commit bfbf79b564
3 changed files with 161 additions and 73 deletions

View File

@ -352,6 +352,27 @@
justify-content: flex-end;
}
.ug-direction-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 999px;
border: 2px solid rgba(255, 255, 255, 0.9);
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.22);
backdrop-filter: blur(4px);
}
.ug-direction-badge.buy {
background: rgba(46, 204, 113, 0.22);
}
.ug-direction-badge.sell {
background: rgba(231, 76, 60, 0.22);
}
.ug-pill {
display: inline-block;
padding: 4px 8px;
@ -364,18 +385,29 @@
.ug-direction-icon {
flex: 0 0 auto;
margin-right: 2px;
opacity: 0.95;
}
.ug-direction-icon.buy {
.ug-direction-icon {
color: rgba(255, 255, 255, 0.95);
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.15));
}
.ug-direction-icon.sell {
color: rgba(255, 255, 255, 0.95);
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.15));
.ug-entry-status {
margin-left: 6px;
font-size: 11px;
padding: 2px 6px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.ug-entry-status.hit {
background: rgba(255, 255, 255, 0.18);
}
.ug-entry-status.wait {
opacity: 0.9;
}
.ug-pill.entry {

View File

@ -185,12 +185,34 @@ function Recommendations() {
const formatTime = (timeStr) => {
if (!timeStr) return '-'
try {
//
// UTC
const date = new Date(timeStr)
// epoch
// "2026-01-19T05:54:04" "2026-01-19 05:54:04"
//
const raw = String(timeStr).trim()
let date = null
// 1) seconds/ms
if (/^\d+$/.test(raw)) {
const n = Number(raw)
const ms = raw.length >= 13 ? n : n * 1000
date = new Date(ms)
} else {
// 2) "YYYY-MM-DD HH:mm:ss" -> UTC
if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(\.\d+)?$/.test(raw)) {
const isoUtc = raw.replace(' ', 'T') + 'Z'
date = new Date(isoUtc)
} else if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?$/.test(raw)) {
// 3) ISO -> UTC
date = new Date(raw + 'Z')
} else {
// 4) Z / +08:00 / RFC1123
date = new Date(raw)
}
}
//
if (isNaN(date.getTime())) {
if (!date || isNaN(date.getTime())) {
return timeStr
}
@ -209,6 +231,20 @@ function Recommendations() {
}
}
const getEntryStatus = (rec) => {
try {
const entry = Number(rec?.suggested_limit_price ?? rec?.planned_entry_price)
const cur = Number(rec?.current_price)
if (!isFinite(entry) || !isFinite(cur) || !rec?.direction) return null
if (rec.direction === 'BUY') {
return cur <= entry ? { text: '已到价', cls: 'hit' } : { text: '未到价', cls: 'wait' }
}
return cur >= entry ? { text: '已到价', cls: 'hit' } : { text: '未到价', cls: 'wait' }
} catch (e) {
return null
}
}
const formatTimeMs = (ms) => {
if (!ms && ms !== 0) return '-'
try {
@ -346,7 +382,9 @@ function Recommendations() {
<span className={`signal-strength ${getSignalStrengthColor(rec.signal_strength)}`}>
信号强度: {rec.signal_strength}/10
</span>
<span className="time">{formatTime(rec.recommendation_time)}</span>
<span className="time" title="北京时间">
{formatTime(rec.recommendation_time)}北京
</span>
</div>
</div>
@ -407,13 +445,19 @@ function Recommendations() {
<div className="user-guide-head">
<strong>📋 操作计划</strong>
<div className="user-guide-prices">
<span className={`ug-direction-badge ${rec.direction === 'BUY' ? 'buy' : 'sell'}`} title={rec.direction === 'BUY' ? '做多' : '做空'}>
{rec.direction === 'BUY' ? (
<TrendingUp className="ug-direction-icon buy" aria-label="做多" size={18} />
<TrendingUp className="ug-direction-icon" aria-label="做多" size={18} />
) : (
<TrendingDown className="ug-direction-icon sell" aria-label="做空" size={18} />
<TrendingDown className="ug-direction-icon" aria-label="做空" size={18} />
)}
</span>
<span className="ug-pill entry">
入场 {fmtPrice(rec.suggested_limit_price ?? rec.planned_entry_price)} USDT
{(() => {
const st = getEntryStatus(rec)
return st ? <span className={`ug-entry-status ${st.cls}`}>{st.text}</span> : null
})()}
</span>
<span className="ug-pill stop">
止损 {fmtPrice(rec.suggested_stop_loss)} USDT
@ -430,62 +474,6 @@ function Recommendations() {
</div>
)}
{/* 交易教程(如果存在) */}
{rec.trading_tutorial && (
<div className="trading-tutorial">
<strong>💡 交易提示:</strong>
<p>{rec.trading_tutorial}</p>
</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 limit">
限价单
</span>
</div>
{rec.suggested_limit_price && (
<div className="param-item limit-price highlight">
<label>建议挂单价:</label>
<span className="limit-price-value highlight-price">
{fmtPrice(rec.suggested_limit_price)} 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>{fmtPrice(rec.suggested_stop_loss)}</span>
</div>
<div className="param-item">
<label>第一目标:</label>
<span>{fmtPrice(rec.suggested_take_profit_1)}</span>
</div>
<div className="param-item">
<label>第二目标:</label>
<span>{fmtPrice(rec.suggested_take_profit_2)}</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>
<div className="card-actions">
{typeFilter === 'realtime' && (
<button
@ -523,6 +511,70 @@ function Recommendations() {
{showDetails[rec.id || rec.symbol] && (
<div className="details-panel">
<div className="details-section">
<h4>交易提示</h4>
<div className="market-info">
<div className="info-item">
<label>提示:</label>
<span>{rec.trading_tutorial || '-'}</span>
</div>
</div>
</div>
<div className="details-section">
<h4>技术分析原因</h4>
<div className="market-info">
<div className="info-item">
<label>原因:</label>
<span>{rec.recommendation_reason || '-'}</span>
</div>
</div>
</div>
<div className="details-section">
<h4>建议参数</h4>
<div className="suggested-params">
<div className="param-item order-type">
<label>订单类型:</label>
<span className="order-type-badge limit">限价单</span>
</div>
{rec.suggested_limit_price && (
<div className="param-item limit-price highlight">
<label>建议挂单价:</label>
<span className="limit-price-value highlight-price">
{fmtPrice(rec.suggested_limit_price)} 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>{fmtPrice(rec.suggested_stop_loss)}</span>
</div>
<div className="param-item">
<label>第一目标:</label>
<span>{fmtPrice(rec.suggested_take_profit_1)}</span>
</div>
<div className="param-item">
<label>第二目标:</label>
<span>{fmtPrice(rec.suggested_take_profit_2)}</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>
</div>
<div className="details-section">
<h4>技术指标</h4>
<div className="indicators-grid">

View File

@ -5,7 +5,7 @@ import asyncio
import logging
import time
from typing import List, Dict, Optional
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
try:
from .binance_client import BinanceClient
from .market_scanner import MarketScanner
@ -19,6 +19,9 @@ except ImportError:
logger = logging.getLogger(__name__)
# 北京时间UTC+8用于推荐时间戳展示避免无时区 isoformat 导致前端误判)
BEIJING_TZ = timezone(timedelta(hours=8))
# 尝试导入数据库模型
DB_AVAILABLE = False
TradeRecommendation = None
@ -470,7 +473,8 @@ class TradeRecommender:
# 添加时间戳
timestamp = time.time()
recommendation_time = datetime.now().isoformat()
# 用带时区的 ISO 字符串,前端可稳定转换/展示北京时间
recommendation_time = datetime.now(BEIJING_TZ).isoformat()
# 计算胜率预估(基于信号强度、市场状态等)
base_win_rate = 40 + (signal_strength * 3) # 信号强度0-10对应胜率40-70%