This commit is contained in:
薇薇安 2026-01-15 20:02:48 +08:00
parent fe034891c8
commit e00cbeb1fe
6 changed files with 419 additions and 124 deletions

View File

@ -14,49 +14,114 @@ router = APIRouter(prefix="/api/recommendations", tags=["recommendations"])
@router.get("")
async def get_recommendations(
status: Optional[str] = Query(None, description="状态过滤: active, executed, expired, cancelled"),
type: Optional[str] = Query('realtime', description="类型: realtime(实时推荐), bookmarked(已标记的推荐)"),
status: Optional[str] = Query(None, description="状态过滤: active, executed, expired, cancelled (仅用于bookmarked类型)"),
direction: Optional[str] = Query(None, description="方向过滤: BUY, SELL"),
limit: int = Query(50, ge=1, le=200, description="返回数量限制"),
start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"),
end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)")
start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD, 仅用于bookmarked类型)"),
end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD, 仅用于bookmarked类型)"),
min_signal_strength: int = Query(5, ge=0, le=10, description="最小信号强度 (仅用于realtime类型)")
):
"""
获取推荐交易对列表
Args:
status: 状态过滤
type: 类型 - realtime(实时推荐基于当前行情), bookmarked(已标记的推荐从数据库查询)
status: 状态过滤仅用于bookmarked类型
direction: 方向过滤
limit: 返回数量限制
start_date: 开始日期
end_date: 结束日期
start_date: 开始日期仅用于bookmarked类型
end_date: 结束日期仅用于bookmarked类型
min_signal_strength: 最小信号强度仅用于realtime类型
"""
try:
# 转换日期字符串
start_dt = None
end_dt = None
if start_date:
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
except ValueError:
raise HTTPException(status_code=400, detail="开始日期格式错误,应为 YYYY-MM-DD")
if end_date:
try:
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
# 设置为当天的23:59:59
end_dt = end_dt.replace(hour=23, minute=59, second=59)
except ValueError:
raise HTTPException(status_code=400, detail="结束日期格式错误,应为 YYYY-MM-DD")
if type == 'realtime':
# 实时生成推荐
import sys
from pathlib import Path
current_file = Path(__file__)
backend_path = current_file.parent.parent.parent
project_root = backend_path.parent
trading_system_path = project_root / 'trading_system'
recommendations = TradeRecommendation.get_all(
status=status,
direction=direction,
limit=limit,
start_date=start_dt,
end_date=end_dt
)
if not trading_system_path.exists():
alternative_path = backend_path / 'trading_system'
if alternative_path.exists():
trading_system_path = alternative_path
else:
raise HTTPException(
status_code=500,
detail=f"交易系统模块不存在"
)
# 如果是获取有效推荐,尝试更新实时价格
if status == 'active' or (status is None and len(recommendations) > 0):
sys.path.insert(0, str(trading_system_path))
sys.path.insert(0, str(project_root))
from binance_client import BinanceClient
from market_scanner import MarketScanner
from risk_manager import RiskManager
from trade_recommender import TradeRecommender
import config
# 初始化组件
client = BinanceClient(
api_key=config.BINANCE_API_KEY,
api_secret=config.BINANCE_API_SECRET,
testnet=config.USE_TESTNET
)
await client.connect()
try:
scanner = MarketScanner(client)
risk_manager = RiskManager(client)
recommender = TradeRecommender(client, scanner, risk_manager)
# 生成推荐
recommendations = await recommender.generate_recommendations(
min_signal_strength=min_signal_strength,
max_recommendations=limit
)
# 方向过滤
if direction:
recommendations = [r for r in recommendations if r.get('direction') == direction]
return {
"success": True,
"count": len(recommendations),
"type": "realtime",
"data": recommendations
}
finally:
await client.disconnect()
elif type == 'bookmarked':
# 从数据库查询已标记的推荐
# 转换日期字符串
start_dt = None
end_dt = None
if start_date:
try:
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
except ValueError:
raise HTTPException(status_code=400, detail="开始日期格式错误,应为 YYYY-MM-DD")
if end_date:
try:
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
end_dt = end_dt.replace(hour=23, minute=59, second=59)
except ValueError:
raise HTTPException(status_code=400, detail="结束日期格式错误,应为 YYYY-MM-DD")
recommendations = TradeRecommendation.get_all(
status=status,
direction=direction,
limit=limit,
start_date=start_dt,
end_date=end_dt
)
# 更新实时价格
try:
import sys
from pathlib import Path
@ -70,29 +135,49 @@ async def get_recommendations(
from binance_client import BinanceClient
import config
# 创建客户端实例(不需要连接,只需要访问价格缓存)
client = BinanceClient(
api_key=config.BINANCE_API_KEY,
api_secret=config.BINANCE_API_SECRET,
testnet=config.USE_TESTNET
)
# 连接Redis如果还没有连接
try:
await client.redis_cache.connect()
except:
pass
# 更新推荐中的实时价格和涨跌幅
for rec in recommendations:
symbol = rec.get('symbol')
if symbol:
# 尝试从WebSocket缓存获取实时价格
# 先尝试从内存缓存获取实时价格(同步)
realtime_price = client.get_realtime_price(symbol)
if realtime_price is not None:
# 更新价格
# 如果内存缓存没有尝试从Valkey异步读取
if realtime_price is None:
try:
realtime_price = await client.get_realtime_price_async(symbol)
except:
pass
# 如果WebSocket/Valkey都没有尝试从REST API获取
if realtime_price is None:
try:
ticker = await client.get_ticker_24h(symbol)
if ticker:
realtime_price = float(ticker.get('lastPrice', 0))
except:
pass
if realtime_price is not None and realtime_price > 0:
old_price = rec.get('current_price', 0)
rec['current_price'] = realtime_price
# 计算新的涨跌幅
if old_price > 0:
change_percent = ((realtime_price - old_price) / old_price) * 100
rec['change_percent'] = round(change_percent, 4)
rec['price_updated'] = True # 标记价格已更新
rec['price_updated'] = True
else:
rec['price_updated'] = False
else:
@ -100,14 +185,19 @@ async def get_recommendations(
else:
rec['price_updated'] = False
except Exception as price_update_error:
# 如果价格更新失败,不影响返回推荐列表
logger.debug(f"更新推荐实时价格失败(不影响返回): {price_update_error}")
return {
"success": True,
"count": len(recommendations),
"data": recommendations
}
return {
"success": True,
"count": len(recommendations),
"type": "bookmarked",
"data": recommendations
}
else:
raise HTTPException(status_code=400, detail="type参数必须是 'realtime''bookmarked'")
except HTTPException:
raise
except Exception as e:
logger.error(f"获取推荐列表失败: {e}")
raise HTTPException(status_code=500, detail=f"获取推荐列表失败: {str(e)}")
@ -116,7 +206,7 @@ async def get_recommendations(
@router.get("/active")
async def get_active_recommendations():
"""
获取当前有效的推荐未过期未执行未取消
获取当前有效的推荐已标记的推荐中未过期未执行未取消
使用WebSocket实时价格更新推荐中的价格信息
同一交易对只返回最新的推荐已去重
"""
@ -128,11 +218,7 @@ async def get_active_recommendations():
for rec in recommendations:
if rec.get('recommendation_time'):
if isinstance(rec['recommendation_time'], datetime):
# 如果是datetime对象转换为ISO格式字符串UTC+8
rec['recommendation_time'] = rec['recommendation_time'].isoformat()
elif isinstance(rec['recommendation_time'], str):
# 如果已经是字符串,确保格式正确
pass
# 尝试从WebSocket缓存获取实时价格更新推荐中的价格
try:
@ -148,29 +234,49 @@ async def get_active_recommendations():
from binance_client import BinanceClient
import config
# 创建客户端实例(不需要连接,只需要访问价格缓存)
client = BinanceClient(
api_key=config.BINANCE_API_KEY,
api_secret=config.BINANCE_API_SECRET,
testnet=config.USE_TESTNET
)
# 连接Redis如果还没有连接
try:
await client.redis_cache.connect()
except:
pass
# 更新推荐中的实时价格和涨跌幅
for rec in recommendations:
symbol = rec.get('symbol')
if symbol:
# 尝试从WebSocket缓存获取实时价格
# 先尝试从内存缓存获取实时价格(同步)
realtime_price = client.get_realtime_price(symbol)
if realtime_price is not None:
# 更新价格
# 如果内存缓存没有尝试从Valkey异步读取
if realtime_price is None:
try:
realtime_price = await client.get_realtime_price_async(symbol)
except:
pass
# 如果WebSocket/Valkey都没有尝试从REST API获取
if realtime_price is None:
try:
ticker = await client.get_ticker_24h(symbol)
if ticker:
realtime_price = float(ticker.get('lastPrice', 0))
except:
pass
if realtime_price is not None and realtime_price > 0:
old_price = rec.get('current_price', 0)
rec['current_price'] = realtime_price
# 计算新的涨跌幅
if old_price > 0:
change_percent = ((realtime_price - old_price) / old_price) * 100
rec['change_percent'] = round(change_percent, 4)
rec['price_updated'] = True # 标记价格已更新
rec['price_updated'] = True
else:
rec['price_updated'] = False
else:
@ -178,7 +284,6 @@ async def get_active_recommendations():
else:
rec['price_updated'] = False
except Exception as price_update_error:
# 如果价格更新失败,不影响返回推荐列表
logger.debug(f"更新推荐实时价格失败(不影响返回): {price_update_error}")
return {
@ -387,3 +492,66 @@ async def generate_recommendations(
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"生成推荐失败: {str(e)}")
@router.post("/bookmark")
async def bookmark_recommendation(recommendation_data: dict):
"""
标记推荐到数据库用于复盘
Args:
recommendation_data: 推荐数据包含所有推荐字段
"""
try:
# 提取必需字段
required_fields = ['symbol', 'direction', 'current_price', 'change_percent',
'recommendation_reason', 'signal_strength']
for field in required_fields:
if field not in recommendation_data:
raise HTTPException(status_code=400, detail=f"缺少必需字段: {field}")
# 保存到数据库
recommendation_id = TradeRecommendation.create(
symbol=recommendation_data.get('symbol'),
direction=recommendation_data.get('direction'),
current_price=recommendation_data.get('current_price'),
change_percent=recommendation_data.get('change_percent'),
recommendation_reason=recommendation_data.get('recommendation_reason'),
signal_strength=recommendation_data.get('signal_strength'),
market_regime=recommendation_data.get('market_regime'),
trend_4h=recommendation_data.get('trend_4h'),
rsi=recommendation_data.get('rsi'),
macd_histogram=recommendation_data.get('macd_histogram'),
bollinger_upper=recommendation_data.get('bollinger_upper'),
bollinger_middle=recommendation_data.get('bollinger_middle'),
bollinger_lower=recommendation_data.get('bollinger_lower'),
ema20=recommendation_data.get('ema20'),
ema50=recommendation_data.get('ema50'),
ema20_4h=recommendation_data.get('ema20_4h'),
atr=recommendation_data.get('atr'),
suggested_stop_loss=recommendation_data.get('suggested_stop_loss'),
suggested_take_profit_1=recommendation_data.get('suggested_take_profit_1'),
suggested_take_profit_2=recommendation_data.get('suggested_take_profit_2'),
suggested_position_percent=recommendation_data.get('suggested_position_percent'),
suggested_leverage=recommendation_data.get('suggested_leverage', 10),
order_type=recommendation_data.get('order_type', 'LIMIT'),
suggested_limit_price=recommendation_data.get('suggested_limit_price'),
volume_24h=recommendation_data.get('volume_24h'),
volatility=recommendation_data.get('volatility'),
notes=recommendation_data.get('notes', '用户标记用于复盘')
)
logger.info(f"✓ 推荐已标记到数据库: {recommendation_data.get('symbol')} {recommendation_data.get('direction')} (ID: {recommendation_id})")
return {
"success": True,
"message": "推荐已标记到数据库",
"recommendation_id": recommendation_id
}
except HTTPException:
raise
except Exception as e:
logger.error(f"标记推荐失败: {e}")
import traceback
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=f"标记推荐失败: {str(e)}")

View File

@ -242,6 +242,7 @@ class TradeRecommendation:
ema20=None, ema50=None, ema20_4h=None, atr=None,
suggested_stop_loss=None, suggested_take_profit_1=None, suggested_take_profit_2=None,
suggested_position_percent=None, suggested_leverage=10,
order_type='LIMIT', suggested_limit_price=None,
volume_24h=None, volatility=None, notes=None
):
"""创建推荐记录(使用北京时间)"""
@ -249,24 +250,54 @@ class TradeRecommendation:
# 默认24小时后过期
expires_at = recommendation_time + timedelta(hours=24)
db.execute_update(
"""INSERT INTO trade_recommendations
(symbol, direction, recommendation_time, current_price, change_percent,
recommendation_reason, signal_strength, market_regime, trend_4h,
rsi, macd_histogram, bollinger_upper, bollinger_middle, bollinger_lower,
ema20, ema50, ema20_4h, atr,
suggested_stop_loss, suggested_take_profit_1, suggested_take_profit_2,
suggested_position_percent, suggested_leverage,
volume_24h, volatility, expires_at, notes)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
(symbol, direction, recommendation_time, current_price, change_percent,
recommendation_reason, signal_strength, market_regime, trend_4h,
rsi, macd_histogram, bollinger_upper, bollinger_middle, bollinger_lower,
ema20, ema50, ema20_4h, atr,
suggested_stop_loss, suggested_take_profit_1, suggested_take_profit_2,
suggested_position_percent, suggested_leverage,
volume_24h, volatility, expires_at, notes)
)
# 检查字段是否存在兼容旧数据库schema
try:
db.execute_one("SELECT order_type FROM trade_recommendations LIMIT 1")
has_order_fields = True
except:
has_order_fields = False
if has_order_fields:
db.execute_update(
"""INSERT INTO trade_recommendations
(symbol, direction, recommendation_time, current_price, change_percent,
recommendation_reason, signal_strength, market_regime, trend_4h,
rsi, macd_histogram, bollinger_upper, bollinger_middle, bollinger_lower,
ema20, ema50, ema20_4h, atr,
suggested_stop_loss, suggested_take_profit_1, suggested_take_profit_2,
suggested_position_percent, suggested_leverage,
order_type, suggested_limit_price,
volume_24h, volatility, expires_at, notes)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
(symbol, direction, recommendation_time, current_price, change_percent,
recommendation_reason, signal_strength, market_regime, trend_4h,
rsi, macd_histogram, bollinger_upper, bollinger_middle, bollinger_lower,
ema20, ema50, ema20_4h, atr,
suggested_stop_loss, suggested_take_profit_1, suggested_take_profit_2,
suggested_position_percent, suggested_leverage,
order_type, suggested_limit_price,
volume_24h, volatility, expires_at, notes)
)
else:
# 兼容旧schema
db.execute_update(
"""INSERT INTO trade_recommendations
(symbol, direction, recommendation_time, current_price, change_percent,
recommendation_reason, signal_strength, market_regime, trend_4h,
rsi, macd_histogram, bollinger_upper, bollinger_middle, bollinger_lower,
ema20, ema50, ema20_4h, atr,
suggested_stop_loss, suggested_take_profit_1, suggested_take_profit_2,
suggested_position_percent, suggested_leverage,
volume_24h, volatility, expires_at, notes)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
(symbol, direction, recommendation_time, current_price, change_percent,
recommendation_reason, signal_strength, market_regime, trend_4h,
rsi, macd_histogram, bollinger_upper, bollinger_middle, bollinger_lower,
ema20, ema50, ema20_4h, atr,
suggested_stop_loss, suggested_take_profit_1, suggested_take_profit_2,
suggested_position_percent, suggested_leverage,
volume_24h, volatility, expires_at, notes)
)
return db.execute_one("SELECT LAST_INSERT_ID() as id")['id']
@staticmethod

View File

@ -288,6 +288,32 @@
font-weight: 500;
}
.card-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-bookmark {
padding: 8px 16px;
background-color: #FF9800;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.btn-bookmark:hover:not(:disabled) {
background-color: #F57C00;
}
.btn-bookmark:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.btn-toggle-details {
padding: 8px 16px;
background-color: #e0e0e0;
@ -296,7 +322,6 @@
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
align-self: flex-start;
}
.btn-toggle-details:hover {
@ -459,7 +484,10 @@
}
.suggested-params {
grid-template-columns: 1fr;
/* grid-template-columns: 1fr; */
display: flex;
width:80vw;
overflow-x: scroll;
}
.indicators-grid {

View File

@ -6,21 +6,23 @@ function Recommendations() {
const [recommendations, setRecommendations] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [statusFilter, setStatusFilter] = useState('active')
const [typeFilter, setTypeFilter] = useState('realtime') // 'realtime' 'bookmarked'
const [statusFilter, setStatusFilter] = useState('')
const [directionFilter, setDirectionFilter] = useState('')
const [generating, setGenerating] = useState(false)
const [showDetails, setShowDetails] = useState({})
const [bookmarking, setBookmarking] = useState({}) // ID
useEffect(() => {
loadRecommendations()
// 10loading
// 10loading
let interval = null
if (statusFilter === 'active') {
if (typeFilter === 'realtime') {
interval = setInterval(async () => {
// loading
try {
const result = await api.getActiveRecommendations()
const result = await api.getRecommendations({ type: 'realtime' })
const newData = result.data || []
// 使setStateloading
@ -30,30 +32,30 @@ function Recommendations() {
return prevRecommendations
}
//
const newDataMap = new Map(newData.map(rec => [rec.id, rec]))
const prevMap = new Map(prevRecommendations.map(rec => [rec.id, rec]))
// id使symbolkey
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 newRec = newDataMap.get(prevRec.id)
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.id))
const newItems = newData.filter(newRec => !prevMap.has(newRec.symbol))
// id
// symbol
const merged = [...updated, ...newItems]
const uniqueMap = new Map()
merged.forEach(rec => {
if (!uniqueMap.has(rec.id)) {
uniqueMap.set(rec.id, rec)
const key = rec.symbol || rec.id
if (!uniqueMap.has(key)) {
uniqueMap.set(key, rec)
}
})
@ -71,26 +73,29 @@ function Recommendations() {
clearInterval(interval)
}
}
}, [statusFilter, directionFilter])
}, [typeFilter, statusFilter, directionFilter])
const loadRecommendations = async () => {
try {
setLoading(true)
setError(null)
let data
if (statusFilter === 'active') {
const result = await api.getActiveRecommendations()
data = result.data || []
} else {
const params = {}
const params = { type: typeFilter }
if (typeFilter === 'bookmarked') {
//
if (statusFilter) params.status = statusFilter
if (directionFilter) params.direction = directionFilter
params.limit = 50
const result = await api.getRecommendations(params)
data = result.data || []
} else {
//
if (directionFilter) params.direction = directionFilter
params.limit = 50
params.min_signal_strength = 5
}
const result = await api.getRecommendations(params)
const data = result.data || []
setRecommendations(data)
} catch (err) {
setError(err.message)
@ -100,6 +105,31 @@ function Recommendations() {
}
}
const handleBookmark = async (rec) => {
try {
setBookmarking(prev => ({ ...prev, [rec.symbol || rec.id]: true }))
await api.bookmarkRecommendation(rec)
//
alert('推荐已标记到数据库,可用于复盘')
//
if (typeFilter === 'bookmarked') {
loadRecommendations()
}
} catch (err) {
alert('标记失败: ' + err.message)
console.error('标记推荐失败:', err)
} finally {
setBookmarking(prev => {
const newState = { ...prev }
delete newState[rec.symbol || rec.id]
return newState
})
}
}
const handleGenerate = async () => {
try {
setGenerating(true)
@ -215,17 +245,31 @@ function Recommendations() {
<div className="filters">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
value={typeFilter}
onChange={(e) => {
setTypeFilter(e.target.value)
setStatusFilter('') //
}}
className="filter-select"
>
<option value="active">有效推荐</option>
<option value="">全部</option>
<option value="executed">已执行</option>
<option value="expired">已过期</option>
<option value="cancelled">已取消</option>
<option value="realtime">实时推荐</option>
<option value="bookmarked">已标记推荐</option>
</select>
{typeFilter === 'bookmarked' && (
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="filter-select"
>
<option value="">全部状态</option>
<option value="active">有效</option>
<option value="executed">已执行</option>
<option value="expired">已过期</option>
<option value="cancelled">已取消</option>
</select>
)}
<select
value={directionFilter}
onChange={(e) => setDirectionFilter(e.target.value)}
@ -255,7 +299,7 @@ function Recommendations() {
) : (
<div className="recommendations-list">
{recommendations.map((rec) => (
<div key={rec.id} className="recommendation-card">
<div key={rec.id || rec.symbol} className="recommendation-card">
<div className="card-header">
<div className="card-title">
<span className="symbol">{rec.symbol}</span>
@ -321,14 +365,26 @@ function Recommendations() {
</div>
</div>
<button
className="btn-toggle-details"
onClick={() => toggleDetails(rec.id)}
>
{showDetails[rec.id] ? '隐藏' : '显示'}详细信息
</button>
<div className="card-actions">
{typeFilter === 'realtime' && (
<button
className="btn-bookmark"
onClick={() => handleBookmark(rec)}
disabled={bookmarking[rec.symbol || rec.id]}
title="标记到数据库用于复盘"
>
{bookmarking[rec.symbol || rec.id] ? '标记中...' : '📌 标记'}
</button>
)}
<button
className="btn-toggle-details"
onClick={() => toggleDetails(rec.id || rec.symbol)}
>
{showDetails[rec.id || rec.symbol] ? '隐藏' : '显示'}详细信息
</button>
</div>
{showDetails[rec.id] && (
{showDetails[rec.id || rec.symbol] && (
<div className="details-panel">
<div className="details-section">
<h4>技术指标</h4>

View File

@ -123,6 +123,10 @@ 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);
@ -133,6 +137,19 @@ export const api = {
return response.json();
},
bookmarkRecommendation: async (recommendationData) => {
const response = await fetch(buildUrl('/api/recommendations/bookmark'), {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(recommendationData)
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '标记推荐失败' }));
throw new Error(error.detail || '标记推荐失败');
}
return response.json();
},
getActiveRecommendations: async () => {
const response = await fetch(buildUrl('/api/recommendations/active'));
if (!response.ok) {

View File

@ -310,17 +310,12 @@ class TradeRecommender:
'volatility': symbol_info.get('volatility')
}
# 保存到数据库
if DB_AVAILABLE and TradeRecommendation:
try:
recommendation_id = TradeRecommendation.create(**recommendation_data)
logger.info(
f"✓ 推荐已保存: {symbol} {direction} "
f"(信号强度: {signal_strength}/10, ID: {recommendation_id})"
)
recommendation_data['id'] = recommendation_id
except Exception as e:
logger.error(f"保存推荐到数据库失败: {e}")
# 不再自动保存到数据库,只返回推荐数据
# 只有用户在前端点击"标记"时才会保存到数据库(用于复盘)
logger.debug(
f"✓ 生成推荐: {symbol} {direction} "
f"(信号强度: {signal_strength}/10)"
)
return recommendation_data