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; 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 { .ug-pill {
display: inline-block; display: inline-block;
padding: 4px 8px; padding: 4px 8px;
@ -364,18 +385,29 @@
.ug-direction-icon { .ug-direction-icon {
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 2px;
opacity: 0.95; opacity: 0.95;
} }
.ug-direction-icon.buy { .ug-direction-icon {
color: rgba(255, 255, 255, 0.95); color: rgba(255, 255, 255, 0.95);
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.15)); filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.15));
} }
.ug-direction-icon.sell { .ug-entry-status {
color: rgba(255, 255, 255, 0.95); margin-left: 6px;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.15)); 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 { .ug-pill.entry {

View File

@ -185,12 +185,34 @@ function Recommendations() {
const formatTime = (timeStr) => { const formatTime = (timeStr) => {
if (!timeStr) return '-' if (!timeStr) return '-'
try { try {
// 处理时区问题:如果时间字符串包含时区信息,直接解析 // 统一按“北京时间”显示,但要先把输入解析成正确的时间点(epoch)
// 否则假设是UTC时间,转换为本地时间 // 注意:部分来源会给出不带时区的字符串(如 "2026-01-19T05:54:04" 或 "2026-01-19 05:54:04")
const date = new Date(timeStr) // 这类字符串在浏览器里会被当作“本地时间”解析,导致显示偏差。
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 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) => { const formatTimeMs = (ms) => {
if (!ms && ms !== 0) return '-' if (!ms && ms !== 0) return '-'
try { try {
@ -346,7 +382,9 @@ function Recommendations() {
<span className={`signal-strength ${getSignalStrengthColor(rec.signal_strength)}`}> <span className={`signal-strength ${getSignalStrengthColor(rec.signal_strength)}`}>
信号强度: {rec.signal_strength}/10 信号强度: {rec.signal_strength}/10
</span> </span>
<span className="time">{formatTime(rec.recommendation_time)}</span> <span className="time" title="北京时间">
{formatTime(rec.recommendation_time)}(北京)
</span>
</div> </div>
</div> </div>
@ -407,13 +445,19 @@ function Recommendations() {
<div className="user-guide-head"> <div className="user-guide-head">
<strong>📋 操作计划</strong> <strong>📋 操作计划</strong>
<div className="user-guide-prices"> <div className="user-guide-prices">
<span className={`ug-direction-badge ${rec.direction === 'BUY' ? 'buy' : 'sell'}`} title={rec.direction === 'BUY' ? '做多' : '做空'}>
{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"> <span className="ug-pill entry">
入场 {fmtPrice(rec.suggested_limit_price ?? rec.planned_entry_price)} USDT 入场 {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>
<span className="ug-pill stop"> <span className="ug-pill stop">
止损 {fmtPrice(rec.suggested_stop_loss)} USDT 止损 {fmtPrice(rec.suggested_stop_loss)} USDT
@ -430,62 +474,6 @@ function Recommendations() {
</div> </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"> <div className="card-actions">
{typeFilter === 'realtime' && ( {typeFilter === 'realtime' && (
<button <button
@ -523,6 +511,70 @@ function Recommendations() {
{showDetails[rec.id || rec.symbol] && ( {showDetails[rec.id || rec.symbol] && (
<div className="details-panel"> <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"> <div className="details-section">
<h4>技术指标</h4> <h4>技术指标</h4>
<div className="indicators-grid"> <div className="indicators-grid">

View File

@ -5,7 +5,7 @@ import asyncio
import logging import logging
import time import time
from typing import List, Dict, Optional from typing import List, Dict, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
try: try:
from .binance_client import BinanceClient from .binance_client import BinanceClient
from .market_scanner import MarketScanner from .market_scanner import MarketScanner
@ -19,6 +19,9 @@ except ImportError:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 北京时间(UTC+8),用于推荐时间戳展示(避免无时区 isoformat 导致前端误判)
BEIJING_TZ = timezone(timedelta(hours=8))
# 尝试导入数据库模型 # 尝试导入数据库模型
DB_AVAILABLE = False DB_AVAILABLE = False
TradeRecommendation = None TradeRecommendation = None
@ -470,7 +473,8 @@ class TradeRecommender:
# 添加时间戳 # 添加时间戳
timestamp = time.time() 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% base_win_rate = 40 + (signal_strength * 3) # 信号强度0-10,对应胜率40-70%