a
This commit is contained in:
parent
63687e9526
commit
bfbf79b564
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
{rec.direction === 'BUY' ? (
|
<span className={`ug-direction-badge ${rec.direction === 'BUY' ? 'buy' : 'sell'}`} title={rec.direction === 'BUY' ? '做多' : '做空'}>
|
||||||
<TrendingUp className="ug-direction-icon buy" aria-label="做多" size={18} />
|
{rec.direction === 'BUY' ? (
|
||||||
) : (
|
<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">
|
||||||
|
|
|
||||||
|
|
@ -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%
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user