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%