a
This commit is contained in:
parent
2539ebc300
commit
b08d97b442
|
|
@ -3,7 +3,7 @@ FastAPI应用主入口
|
|||
"""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from api.routes import config, trades, stats, dashboard, account
|
||||
from api.routes import config, trades, stats, dashboard, account, recommendations
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
|
@ -118,6 +118,7 @@ app.include_router(trades.router, prefix="/api/trades", tags=["交易记录"])
|
|||
app.include_router(stats.router, prefix="/api/stats", tags=["统计分析"])
|
||||
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"])
|
||||
app.include_router(account.router, prefix="/api/account", tags=["账户数据"])
|
||||
app.include_router(recommendations.router, tags=["交易推荐"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
|
|
|||
263
backend/api/routes/recommendations.py
Normal file
263
backend/api/routes/recommendations.py
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
"""
|
||||
推荐交易对API路由
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
from database.models import TradeRecommendation
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/recommendations", tags=["recommendations"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_recommendations(
|
||||
status: Optional[str] = Query(None, description="状态过滤: active, executed, expired, cancelled"),
|
||||
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)")
|
||||
):
|
||||
"""
|
||||
获取推荐交易对列表
|
||||
|
||||
Args:
|
||||
status: 状态过滤
|
||||
direction: 方向过滤
|
||||
limit: 返回数量限制
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
"""
|
||||
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")
|
||||
|
||||
recommendations = TradeRecommendation.get_all(
|
||||
status=status,
|
||||
direction=direction,
|
||||
limit=limit,
|
||||
start_date=start_dt,
|
||||
end_date=end_dt
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"count": len(recommendations),
|
||||
"data": recommendations
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取推荐列表失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取推荐列表失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/active")
|
||||
async def get_active_recommendations():
|
||||
"""
|
||||
获取当前有效的推荐(未过期、未执行、未取消)
|
||||
"""
|
||||
try:
|
||||
recommendations = TradeRecommendation.get_active()
|
||||
return {
|
||||
"success": True,
|
||||
"count": len(recommendations),
|
||||
"data": recommendations
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取有效推荐失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取有效推荐失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{recommendation_id}")
|
||||
async def get_recommendation(recommendation_id: int):
|
||||
"""
|
||||
根据ID获取推荐详情
|
||||
"""
|
||||
try:
|
||||
recommendation = TradeRecommendation.get_by_id(recommendation_id)
|
||||
if not recommendation:
|
||||
raise HTTPException(status_code=404, detail="推荐不存在")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": recommendation
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取推荐详情失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取推荐详情失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/symbol/{symbol}")
|
||||
async def get_recommendations_by_symbol(
|
||||
symbol: str,
|
||||
limit: int = Query(10, ge=1, le=50, description="返回数量限制")
|
||||
):
|
||||
"""
|
||||
根据交易对获取推荐记录
|
||||
"""
|
||||
try:
|
||||
recommendations = TradeRecommendation.get_by_symbol(symbol, limit=limit)
|
||||
return {
|
||||
"success": True,
|
||||
"count": len(recommendations),
|
||||
"data": recommendations
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取交易对推荐失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取交易对推荐失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{recommendation_id}/execute")
|
||||
async def mark_recommendation_executed(
|
||||
recommendation_id: int,
|
||||
trade_id: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
标记推荐已执行
|
||||
|
||||
Args:
|
||||
recommendation_id: 推荐ID
|
||||
trade_id: 关联的交易记录ID(可选)
|
||||
"""
|
||||
try:
|
||||
recommendation = TradeRecommendation.get_by_id(recommendation_id)
|
||||
if not recommendation:
|
||||
raise HTTPException(status_code=404, detail="推荐不存在")
|
||||
|
||||
if recommendation['status'] != 'active':
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"推荐状态为 {recommendation['status']},无法标记为已执行"
|
||||
)
|
||||
|
||||
TradeRecommendation.mark_executed(recommendation_id, trade_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "推荐已标记为已执行"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"标记推荐已执行失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"标记推荐已执行失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{recommendation_id}/cancel")
|
||||
async def cancel_recommendation(
|
||||
recommendation_id: int,
|
||||
notes: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
取消推荐
|
||||
|
||||
Args:
|
||||
recommendation_id: 推荐ID
|
||||
notes: 取消原因备注
|
||||
"""
|
||||
try:
|
||||
recommendation = TradeRecommendation.get_by_id(recommendation_id)
|
||||
if not recommendation:
|
||||
raise HTTPException(status_code=404, detail="推荐不存在")
|
||||
|
||||
if recommendation['status'] != 'active':
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"推荐状态为 {recommendation['status']},无法取消"
|
||||
)
|
||||
|
||||
TradeRecommendation.mark_cancelled(recommendation_id, notes)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "推荐已取消"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"取消推荐失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"取消推荐失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate_recommendations(
|
||||
min_signal_strength: int = Query(5, ge=0, le=10, description="最小信号强度"),
|
||||
max_recommendations: int = Query(20, ge=1, le=50, description="最大推荐数量")
|
||||
):
|
||||
"""
|
||||
生成新的交易推荐
|
||||
|
||||
Args:
|
||||
min_signal_strength: 最小信号强度
|
||||
max_recommendations: 最大推荐数量
|
||||
"""
|
||||
try:
|
||||
# 导入推荐器
|
||||
import sys
|
||||
from pathlib import Path
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
trading_system_path = project_root / 'trading_system'
|
||||
|
||||
if not trading_system_path.exists():
|
||||
raise HTTPException(status_code=500, detail="交易系统模块不存在")
|
||||
|
||||
sys.path.insert(0, str(trading_system_path))
|
||||
|
||||
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=max_recommendations
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"count": len(recommendations),
|
||||
"message": f"成功生成 {len(recommendations)} 个推荐",
|
||||
"data": recommendations
|
||||
}
|
||||
finally:
|
||||
await client.disconnect()
|
||||
|
||||
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)}")
|
||||
|
|
@ -80,6 +80,51 @@ CREATE TABLE IF NOT EXISTS `trading_signals` (
|
|||
INDEX `idx_executed` (`executed`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易信号表';
|
||||
|
||||
-- 推荐交易对表
|
||||
CREATE TABLE IF NOT EXISTS `trade_recommendations` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`symbol` VARCHAR(20) NOT NULL,
|
||||
`direction` VARCHAR(10) NOT NULL COMMENT 'BUY, SELL',
|
||||
`recommendation_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`current_price` DECIMAL(20, 8) NOT NULL,
|
||||
`change_percent` DECIMAL(10, 4) COMMENT '24小时涨跌幅',
|
||||
`recommendation_reason` TEXT NOT NULL COMMENT '推荐原因',
|
||||
`signal_strength` INT NOT NULL COMMENT '信号强度 0-10',
|
||||
`market_regime` VARCHAR(20) COMMENT 'trending, ranging',
|
||||
`trend_4h` VARCHAR(10) COMMENT '4H周期趋势: up, down, neutral',
|
||||
-- 技术指标
|
||||
`rsi` DECIMAL(10, 4) COMMENT 'RSI指标',
|
||||
`macd_histogram` DECIMAL(20, 8) COMMENT 'MACD柱状图',
|
||||
`bollinger_upper` DECIMAL(20, 8) COMMENT '布林带上轨',
|
||||
`bollinger_middle` DECIMAL(20, 8) COMMENT '布林带中轨',
|
||||
`bollinger_lower` DECIMAL(20, 8) COMMENT '布林带下轨',
|
||||
`ema20` DECIMAL(20, 8) COMMENT 'EMA20',
|
||||
`ema50` DECIMAL(20, 8) COMMENT 'EMA50',
|
||||
`ema20_4h` DECIMAL(20, 8) COMMENT '4H周期EMA20',
|
||||
`atr` DECIMAL(20, 8) COMMENT '平均真实波幅',
|
||||
-- 建议参数
|
||||
`suggested_stop_loss` DECIMAL(20, 8) COMMENT '建议止损价',
|
||||
`suggested_take_profit_1` DECIMAL(20, 8) COMMENT '建议第一目标止盈价(盈亏比1:1)',
|
||||
`suggested_take_profit_2` DECIMAL(20, 8) COMMENT '建议第二目标止盈价',
|
||||
`suggested_position_percent` DECIMAL(10, 4) COMMENT '建议仓位百分比',
|
||||
`suggested_leverage` INT DEFAULT 10 COMMENT '建议杠杆倍数',
|
||||
-- 市场数据
|
||||
`volume_24h` DECIMAL(20, 8) COMMENT '24小时成交量',
|
||||
`volatility` DECIMAL(10, 4) COMMENT '波动率',
|
||||
-- 状态
|
||||
`status` VARCHAR(20) DEFAULT 'active' COMMENT 'active: 有效, expired: 已过期, executed: 已执行, cancelled: 已取消',
|
||||
`executed_at` TIMESTAMP NULL COMMENT '执行时间',
|
||||
`execution_result` VARCHAR(50) COMMENT '执行结果: success, failed',
|
||||
`execution_trade_id` INT COMMENT '关联的交易记录ID',
|
||||
`expires_at` TIMESTAMP NULL COMMENT '推荐过期时间(默认24小时后)',
|
||||
`notes` TEXT COMMENT '备注信息',
|
||||
INDEX `idx_symbol` (`symbol`),
|
||||
INDEX `idx_recommendation_time` (`recommendation_time`),
|
||||
INDEX `idx_status` (`status`),
|
||||
INDEX `idx_direction` (`direction`),
|
||||
INDEX `idx_signal_strength` (`signal_strength`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐交易对表';
|
||||
|
||||
|
||||
|
||||
-- 初始化默认配置
|
||||
|
|
|
|||
|
|
@ -228,3 +228,121 @@ class TradingSignal:
|
|||
"SELECT * FROM trading_signals ORDER BY signal_time DESC LIMIT %s",
|
||||
(limit,)
|
||||
)
|
||||
|
||||
|
||||
class TradeRecommendation:
|
||||
"""推荐交易对模型"""
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
symbol, direction, current_price, change_percent, recommendation_reason,
|
||||
signal_strength, market_regime=None, trend_4h=None,
|
||||
rsi=None, macd_histogram=None,
|
||||
bollinger_upper=None, bollinger_middle=None, bollinger_lower=None,
|
||||
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,
|
||||
volume_24h=None, volatility=None, notes=None
|
||||
):
|
||||
"""创建推荐记录(使用北京时间)"""
|
||||
recommendation_time = get_beijing_time()
|
||||
# 默认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)
|
||||
)
|
||||
return db.execute_one("SELECT LAST_INSERT_ID() as id")['id']
|
||||
|
||||
@staticmethod
|
||||
def mark_executed(recommendation_id, trade_id=None, execution_result='success'):
|
||||
"""标记推荐已执行"""
|
||||
executed_at = get_beijing_time()
|
||||
db.execute_update(
|
||||
"""UPDATE trade_recommendations
|
||||
SET status = 'executed', executed_at = %s, execution_result = %s, execution_trade_id = %s
|
||||
WHERE id = %s""",
|
||||
(executed_at, execution_result, trade_id, recommendation_id)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def mark_expired(recommendation_id):
|
||||
"""标记推荐已过期"""
|
||||
db.execute_update(
|
||||
"UPDATE trade_recommendations SET status = 'expired' WHERE id = %s",
|
||||
(recommendation_id,)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def mark_cancelled(recommendation_id, notes=None):
|
||||
"""标记推荐已取消"""
|
||||
db.execute_update(
|
||||
"UPDATE trade_recommendations SET status = 'cancelled', notes = %s WHERE id = %s",
|
||||
(notes, recommendation_id)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_all(status=None, direction=None, limit=100, start_date=None, end_date=None):
|
||||
"""获取推荐记录"""
|
||||
query = "SELECT * FROM trade_recommendations WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if status:
|
||||
query += " AND status = %s"
|
||||
params.append(status)
|
||||
if direction:
|
||||
query += " AND direction = %s"
|
||||
params.append(direction)
|
||||
if start_date:
|
||||
query += " AND recommendation_time >= %s"
|
||||
params.append(start_date)
|
||||
if end_date:
|
||||
query += " AND recommendation_time <= %s"
|
||||
params.append(end_date)
|
||||
|
||||
query += " ORDER BY recommendation_time DESC, signal_strength DESC LIMIT %s"
|
||||
params.append(limit)
|
||||
|
||||
return db.execute_query(query, params)
|
||||
|
||||
@staticmethod
|
||||
def get_active():
|
||||
"""获取当前有效的推荐(未过期、未执行、未取消)"""
|
||||
return db.execute_query(
|
||||
"""SELECT * FROM trade_recommendations
|
||||
WHERE status = 'active' AND (expires_at IS NULL OR expires_at > NOW())
|
||||
ORDER BY signal_strength DESC, recommendation_time DESC"""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(recommendation_id):
|
||||
"""根据ID获取推荐"""
|
||||
return db.execute_one(
|
||||
"SELECT * FROM trade_recommendations WHERE id = %s",
|
||||
(recommendation_id,)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_symbol(symbol, limit=10):
|
||||
"""根据交易对获取推荐记录"""
|
||||
return db.execute_query(
|
||||
"""SELECT * FROM trade_recommendations
|
||||
WHERE symbol = %s
|
||||
ORDER BY recommendation_time DESC LIMIT %s""",
|
||||
(symbol, limit)
|
||||
)
|
||||
|
|
|
|||
341
trading_system/trade_recommender.py
Normal file
341
trading_system/trade_recommender.py
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
"""
|
||||
推荐交易对模块 - 生成交易推荐供手动参考
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
try:
|
||||
from .binance_client import BinanceClient
|
||||
from .market_scanner import MarketScanner
|
||||
from .risk_manager import RiskManager
|
||||
from . import config
|
||||
except ImportError:
|
||||
from binance_client import BinanceClient
|
||||
from market_scanner import MarketScanner
|
||||
from risk_manager import RiskManager
|
||||
import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 尝试导入数据库模型
|
||||
DB_AVAILABLE = False
|
||||
TradeRecommendation = None
|
||||
try:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
project_root = Path(__file__).parent.parent
|
||||
backend_path = project_root / 'backend'
|
||||
if backend_path.exists():
|
||||
sys.path.insert(0, str(backend_path))
|
||||
from database.models import TradeRecommendation
|
||||
DB_AVAILABLE = True
|
||||
logger.info("✓ 数据库模型导入成功,推荐记录将保存到数据库")
|
||||
else:
|
||||
logger.warning("⚠ backend目录不存在,无法使用数据库功能")
|
||||
DB_AVAILABLE = False
|
||||
except ImportError as e:
|
||||
logger.warning(f"⚠ 无法导入数据库模型: {e}")
|
||||
DB_AVAILABLE = False
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠ 数据库初始化失败: {e}")
|
||||
DB_AVAILABLE = False
|
||||
|
||||
|
||||
class TradeRecommender:
|
||||
"""推荐交易对生成器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: BinanceClient,
|
||||
scanner: MarketScanner,
|
||||
risk_manager: RiskManager
|
||||
):
|
||||
"""
|
||||
初始化推荐器
|
||||
|
||||
Args:
|
||||
client: 币安客户端
|
||||
scanner: 市场扫描器
|
||||
risk_manager: 风险管理器
|
||||
"""
|
||||
self.client = client
|
||||
self.scanner = scanner
|
||||
self.risk_manager = risk_manager
|
||||
|
||||
async def generate_recommendations(
|
||||
self,
|
||||
min_signal_strength: int = 5,
|
||||
max_recommendations: int = 20
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
生成交易推荐
|
||||
|
||||
Args:
|
||||
min_signal_strength: 最小信号强度(默认5,低于此强度的不推荐)
|
||||
max_recommendations: 最大推荐数量
|
||||
|
||||
Returns:
|
||||
推荐列表
|
||||
"""
|
||||
logger.info("开始生成交易推荐...")
|
||||
|
||||
# 1. 扫描市场
|
||||
top_symbols = await self.scanner.scan_market()
|
||||
if not top_symbols:
|
||||
logger.warning("未找到符合条件的交易对")
|
||||
return []
|
||||
|
||||
recommendations = []
|
||||
|
||||
# 2. 对每个交易对进行分析
|
||||
for symbol_info in top_symbols:
|
||||
if len(recommendations) >= max_recommendations:
|
||||
break
|
||||
|
||||
symbol = symbol_info['symbol']
|
||||
current_price = symbol_info['price']
|
||||
change_percent = symbol_info.get('changePercent', 0)
|
||||
|
||||
# 3. 分析交易信号(使用策略模块的逻辑)
|
||||
trade_signal = await self._analyze_trade_signal(symbol_info)
|
||||
|
||||
# 4. 如果信号强度足够,生成推荐
|
||||
if trade_signal['should_trade'] and trade_signal['strength'] >= min_signal_strength:
|
||||
recommendation = await self._create_recommendation(
|
||||
symbol_info, trade_signal
|
||||
)
|
||||
if recommendation:
|
||||
recommendations.append(recommendation)
|
||||
|
||||
logger.info(f"生成了 {len(recommendations)} 个交易推荐")
|
||||
return recommendations
|
||||
|
||||
async def _analyze_trade_signal(self, symbol_info: Dict) -> Dict:
|
||||
"""
|
||||
分析交易信号(复用策略模块的逻辑)
|
||||
|
||||
Args:
|
||||
symbol_info: 交易对信息
|
||||
|
||||
Returns:
|
||||
交易信号字典
|
||||
"""
|
||||
symbol = symbol_info['symbol']
|
||||
current_price = symbol_info['price']
|
||||
rsi = symbol_info.get('rsi')
|
||||
macd = symbol_info.get('macd')
|
||||
bollinger = symbol_info.get('bollinger')
|
||||
market_regime = symbol_info.get('marketRegime', 'unknown')
|
||||
ema20 = symbol_info.get('ema20')
|
||||
ema50 = symbol_info.get('ema50')
|
||||
ema20_4h = symbol_info.get('ema20_4h')
|
||||
price_4h = symbol_info.get('price_4h', current_price)
|
||||
|
||||
# 判断4H周期趋势方向
|
||||
trend_4h = None
|
||||
if ema20_4h is not None:
|
||||
if price_4h > ema20_4h:
|
||||
trend_4h = 'up'
|
||||
elif price_4h < ema20_4h:
|
||||
trend_4h = 'down'
|
||||
else:
|
||||
trend_4h = 'neutral'
|
||||
|
||||
signal_strength = 0
|
||||
reasons = []
|
||||
direction = None
|
||||
|
||||
# 策略1:均值回归(震荡市场)
|
||||
if market_regime == 'ranging':
|
||||
if rsi and rsi < 30:
|
||||
if trend_4h in ('up', 'neutral', None):
|
||||
signal_strength += 4
|
||||
reasons.append(f"RSI超卖({rsi:.1f})")
|
||||
if direction is None:
|
||||
direction = 'BUY'
|
||||
elif rsi and rsi > 70:
|
||||
if trend_4h in ('down', 'neutral', None):
|
||||
signal_strength += 4
|
||||
reasons.append(f"RSI超买({rsi:.1f})")
|
||||
if direction is None:
|
||||
direction = 'SELL'
|
||||
|
||||
if bollinger and current_price <= bollinger.get('lower'):
|
||||
if trend_4h in ('up', 'neutral', None):
|
||||
signal_strength += 3
|
||||
reasons.append("触及布林带下轨")
|
||||
if direction is None:
|
||||
direction = 'BUY'
|
||||
elif bollinger and current_price >= bollinger.get('upper'):
|
||||
if trend_4h in ('down', 'neutral', None):
|
||||
signal_strength += 3
|
||||
reasons.append("触及布林带上轨")
|
||||
if direction is None:
|
||||
direction = 'SELL'
|
||||
|
||||
# 策略2:趋势跟踪(趋势市场)
|
||||
elif market_regime == 'trending':
|
||||
if macd and macd.get('macd', 0) > macd.get('signal', 0) and macd.get('histogram', 0) > 0:
|
||||
if trend_4h in ('up', 'neutral', None):
|
||||
signal_strength += 3
|
||||
reasons.append("MACD金叉")
|
||||
if direction is None:
|
||||
direction = 'BUY'
|
||||
elif macd and macd.get('macd', 0) < macd.get('signal', 0) and macd.get('histogram', 0) < 0:
|
||||
if trend_4h in ('down', 'neutral', None):
|
||||
signal_strength += 3
|
||||
reasons.append("MACD死叉")
|
||||
if direction is None:
|
||||
direction = 'SELL'
|
||||
|
||||
if ema20 and ema50:
|
||||
if current_price > ema20 > ema50:
|
||||
if trend_4h in ('up', 'neutral', None):
|
||||
signal_strength += 2
|
||||
reasons.append("价格在均线之上")
|
||||
if direction is None:
|
||||
direction = 'BUY'
|
||||
elif current_price < ema20 < ema50:
|
||||
if trend_4h in ('down', 'neutral', None):
|
||||
signal_strength += 2
|
||||
reasons.append("价格在均线之下")
|
||||
if direction is None:
|
||||
direction = 'SELL'
|
||||
|
||||
# 多周期共振加分
|
||||
if direction and trend_4h:
|
||||
if (direction == 'BUY' and trend_4h == 'up') or (direction == 'SELL' and trend_4h == 'down'):
|
||||
signal_strength += 2
|
||||
reasons.append("4H周期共振确认")
|
||||
|
||||
# 判断是否应该交易
|
||||
min_signal_strength = config.TRADING_CONFIG.get('MIN_SIGNAL_STRENGTH', 7)
|
||||
should_trade = signal_strength >= min_signal_strength
|
||||
|
||||
# 禁止逆4H趋势交易
|
||||
if direction and trend_4h:
|
||||
if (direction == 'BUY' and trend_4h == 'down') or (direction == 'SELL' and trend_4h == 'up'):
|
||||
should_trade = False
|
||||
reasons.append("❌ 禁止逆4H趋势交易")
|
||||
|
||||
return {
|
||||
'should_trade': should_trade,
|
||||
'direction': direction,
|
||||
'reason': ', '.join(reasons) if reasons else '无明确信号',
|
||||
'strength': signal_strength,
|
||||
'trend_4h': trend_4h
|
||||
}
|
||||
|
||||
async def _create_recommendation(
|
||||
self,
|
||||
symbol_info: Dict,
|
||||
trade_signal: Dict
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
创建推荐记录
|
||||
|
||||
Args:
|
||||
symbol_info: 交易对信息
|
||||
trade_signal: 交易信号
|
||||
|
||||
Returns:
|
||||
推荐字典
|
||||
"""
|
||||
try:
|
||||
symbol = symbol_info['symbol']
|
||||
current_price = symbol_info['price']
|
||||
direction = trade_signal['direction']
|
||||
|
||||
# 计算建议的止损止盈
|
||||
entry_price = current_price
|
||||
stop_loss_price = self.risk_manager.get_stop_loss_price(
|
||||
entry_price,
|
||||
direction,
|
||||
klines=symbol_info.get('klines'),
|
||||
bollinger=symbol_info.get('bollinger'),
|
||||
atr=symbol_info.get('atr')
|
||||
)
|
||||
|
||||
# 计算止损百分比
|
||||
if direction == 'BUY':
|
||||
stop_loss_pct = (entry_price - stop_loss_price) / entry_price
|
||||
else:
|
||||
stop_loss_pct = (stop_loss_price - entry_price) / entry_price
|
||||
|
||||
# 第一目标:盈亏比1:1
|
||||
if direction == 'BUY':
|
||||
take_profit_1 = entry_price + (entry_price - stop_loss_price)
|
||||
else:
|
||||
take_profit_1 = entry_price - (stop_loss_price - entry_price)
|
||||
|
||||
# 第二目标:止损的2.5倍
|
||||
take_profit_2_pct = stop_loss_pct * 2.5
|
||||
take_profit_2 = self.risk_manager.get_take_profit_price(
|
||||
entry_price, direction, take_profit_pct=take_profit_2_pct
|
||||
)
|
||||
|
||||
# 建议仓位(根据信号强度调整)
|
||||
base_position_pct = config.TRADING_CONFIG.get('MAX_POSITION_PERCENT', 0.05)
|
||||
signal_strength = trade_signal['strength']
|
||||
# 信号强度越高,建议仓位可以适当增加(但不超过1.5倍)
|
||||
position_multiplier = min(1.0 + (signal_strength - 5) * 0.1, 1.5)
|
||||
suggested_position_pct = base_position_pct * position_multiplier
|
||||
|
||||
# 准备推荐数据
|
||||
recommendation_data = {
|
||||
'symbol': symbol,
|
||||
'direction': direction,
|
||||
'current_price': current_price,
|
||||
'change_percent': symbol_info.get('changePercent', 0),
|
||||
'recommendation_reason': trade_signal['reason'],
|
||||
'signal_strength': signal_strength,
|
||||
'market_regime': symbol_info.get('marketRegime'),
|
||||
'trend_4h': trade_signal.get('trend_4h'),
|
||||
'rsi': symbol_info.get('rsi'),
|
||||
'macd_histogram': symbol_info.get('macd', {}).get('histogram') if symbol_info.get('macd') else None,
|
||||
'bollinger_upper': symbol_info.get('bollinger', {}).get('upper') if symbol_info.get('bollinger') else None,
|
||||
'bollinger_middle': symbol_info.get('bollinger', {}).get('middle') if symbol_info.get('bollinger') else None,
|
||||
'bollinger_lower': symbol_info.get('bollinger', {}).get('lower') if symbol_info.get('bollinger') else None,
|
||||
'ema20': symbol_info.get('ema20'),
|
||||
'ema50': symbol_info.get('ema50'),
|
||||
'ema20_4h': symbol_info.get('ema20_4h'),
|
||||
'atr': symbol_info.get('atr'),
|
||||
'suggested_stop_loss': stop_loss_price,
|
||||
'suggested_take_profit_1': take_profit_1,
|
||||
'suggested_take_profit_2': take_profit_2,
|
||||
'suggested_position_percent': suggested_position_pct,
|
||||
'suggested_leverage': config.TRADING_CONFIG.get('LEVERAGE', 10),
|
||||
'volume_24h': symbol_info.get('volume24h'),
|
||||
'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}")
|
||||
|
||||
return recommendation_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建推荐失败 {symbol_info.get('symbol', 'unknown')}: {e}")
|
||||
return None
|
||||
|
||||
async def get_active_recommendations(self) -> List[Dict]:
|
||||
"""获取当前有效的推荐"""
|
||||
if DB_AVAILABLE and TradeRecommendation:
|
||||
return TradeRecommendation.get_active()
|
||||
return []
|
||||
|
||||
async def mark_recommendation_executed(self, recommendation_id: int, trade_id: int = None):
|
||||
"""标记推荐已执行"""
|
||||
if DB_AVAILABLE and TradeRecommendation:
|
||||
TradeRecommendation.mark_executed(recommendation_id, trade_id)
|
||||
logger.info(f"推荐 {recommendation_id} 已标记为已执行")
|
||||
Loading…
Reference in New Issue
Block a user