Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
TIANHE
2026-03-01 17:20:37 +08:00
parent d60409f7f8
commit db91fa4580
53 changed files with 1596 additions and 611 deletions
@@ -36,6 +36,7 @@ DEFAULT_BILLING_CONFIG = {
'cost_backtest': 3, # 回测 每次消耗积分
'cost_portfolio_monitor': 8, # Portfolio AI监控 每次消耗积分
'cost_indicator_create': 0, # 创建指标 免费
'cost_polymarket_deep_analysis': 15, # Polymarket深度分析 每次消耗积分
}
# Feature name mapping (for log recording)
@@ -45,6 +46,7 @@ FEATURE_NAMES = {
'backtest': 'Backtest',
'portfolio_monitor': 'Portfolio Monitor',
'indicator_create': 'Indicator Create',
'polymarket_deep_analysis': 'Polymarket Deep Analysis',
}
@@ -55,7 +55,7 @@ class FastAnalysisService:
include_macro=True,
include_news=True,
include_polymarket=True, # 包含预测市场数据
timeout=30
timeout=45 # 增加超时时间,确保数据收集完成
)
def _calculate_indicators(self, kline_data: List[Dict]) -> Dict[str, Any]:
@@ -678,10 +678,16 @@ IMPORTANT:
"""
start_time = time.time()
# Get default model if not specified
if not model:
model = self.llm_service.get_default_model()
logger.debug(f"Using default model: {model}")
result = {
"market": market,
"symbol": symbol,
"language": language,
"model": model, # Include model in result from the start
"timeframe": timeframe,
"analysis_time_ms": 0,
"error": None,
@@ -823,6 +829,8 @@ IMPORTANT:
"decision": analysis.get("decision", "HOLD"),
"confidence": analysis.get("confidence", 50),
"summary": analysis.get("summary", ""),
"model": model, # Model is already set in result initialization
"language": language, # Ensure language is included for task record
"detailed_analysis": {
"technical": detailed_analysis.get("technical", ""),
"fundamental": detailed_analysis.get("fundamental", ""),
@@ -1600,11 +1608,81 @@ IMPORTANT:
from app.services.analysis_memory import get_analysis_memory
memory = get_analysis_memory()
memory_id = memory.store(result, user_id=user_id)
# Also save to qd_analysis_tasks for admin statistics
self._save_analysis_task(result, user_id=user_id)
return memory_id
except Exception as e:
logger.warning(f"Memory storage failed: {e}")
return None
def _save_analysis_task(self, result: Dict, user_id: int = None) -> Optional[int]:
"""
Save analysis record to qd_analysis_tasks table for admin statistics.
Args:
result: Analysis result dictionary
user_id: User ID who created this analysis
Returns:
Task ID or None if failed
"""
try:
from app.utils.db import get_db_connection
market = result.get("market", "")
symbol = result.get("symbol", "")
model = result.get("model", "")
# If model is empty, get default model
if not model:
from app.services.llm import LLMService
llm_service = LLMService()
model = llm_service.get_default_model()
language = result.get("language", "en-US")
status = "completed" if not result.get("error") else "failed"
result_json = json.dumps(result, ensure_ascii=False)
error_message = result.get("error", "")
if not market or not symbol:
logger.warning(f"Cannot save analysis task: missing market or symbol")
return None
with get_db_connection() as db:
cur = db.cursor()
# PostgreSQL: Use RETURNING to get the inserted ID
cur.execute(
"""
INSERT INTO qd_analysis_tasks
(user_id, market, symbol, model, language, status, result_json, error_message, created_at, completed_at)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
RETURNING id
""",
(
int(user_id) if user_id else 1, # Default to user 1 if not provided
str(market),
str(symbol),
str(model) if model else '',
str(language),
str(status),
str(result_json),
str(error_message) if error_message else ''
)
)
row = cur.fetchone()
task_id = row['id'] if row else None
db.commit()
cur.close()
if task_id:
logger.debug(f"Saved analysis task {task_id} for user {user_id}: {market}:{symbol}")
return task_id
except Exception as e:
logger.warning(f"Failed to save analysis task: {e}")
return None
# ==================== Backward Compatibility ====================
def analyze_legacy_format(self, market: str, symbol: str, language: str = 'en-US',
@@ -1146,10 +1146,9 @@ class MarketDataCollector:
return news_list
# 搜索全球重大事件(最近24小时)
# 优化:减少搜索次数,只搜索最重要的查询
global_event_queries = [
"war conflict breaking news today",
"geopolitical crisis latest",
"US Iran military news"
"war conflict breaking news today" # 只搜索最重要的查询,减少API调用
]
for query in global_event_queries:
@@ -1226,14 +1225,20 @@ class MarketDataCollector:
keywords = self._extract_polymarket_keywords(symbol, market)
logger.info(f"Extracted Polymarket keywords for {symbol}: {keywords}")
# 直接调用API搜索,不使用数据库缓存
# 因为AI分析需要最新的、相关的数据,数据库不可能包含所有市场
# 优化:使用缓存加速,减少API调用时间
# 对于AI分析,使用短期缓存(5分钟)即可,既保证时效性又提升性能
# 进一步优化:限制关键词数量,只搜索最重要的关键词(最多2个)
related_markets = []
for keyword in keywords:
# 使用use_cache=False强制从API获取
markets = polymarket_source.search_markets(keyword, limit=5, use_cache=False)
logger.info(f"Found {len(markets)} markets for keyword '{keyword}' (from API)")
related_markets.extend(markets)
max_keywords = 2 # 最多只搜索2个关键词,减少API调用
for keyword in keywords[:max_keywords]:
try:
# 使用use_cache=True启用缓存,减少API调用时间
markets = polymarket_source.search_markets(keyword, limit=5, use_cache=True)
logger.info(f"Found {len(markets)} markets for keyword '{keyword}' (cached)")
related_markets.extend(markets)
except Exception as e:
logger.warning(f"Failed to search Polymarket for keyword '{keyword}': {e}")
continue
# 去重
seen = set()
@@ -1271,39 +1276,54 @@ class MarketDataCollector:
return []
def _extract_polymarket_keywords(self, symbol: str, market: str) -> List[str]:
"""提取用于搜索预测市场的关键词"""
"""
提取用于搜索预测市场的关键词
优化:只保留最重要的关键词,减少API调用次数
"""
keywords = []
# 基础符号
# 基础符号(最重要)
if '/' in symbol:
base = symbol.split('/')[0]
keywords.append(base)
else:
keywords.append(symbol)
# 加密货币全名映射
# 加密货币全名映射(只保留一个最重要的全名,避免重复)
crypto_names = {
'BTC': ['Bitcoin', 'bitcoin'],
'ETH': ['Ethereum', 'ethereum'],
'SOL': ['Solana', 'solana'],
'BNB': ['Binance', 'binance'],
'XRP': ['Ripple', 'ripple'],
'ADA': ['Cardano', 'cardano'],
'DOGE': ['Dogecoin', 'dogecoin'],
'AVAX': ['Avalanche', 'avalanche'],
'DOT': ['Polkadot', 'polkadot'],
'MATIC': ['Polygon', 'polygon']
'BTC': 'Bitcoin',
'ETH': 'Ethereum',
'SOL': 'Solana',
'BNB': 'Binance',
'XRP': 'Ripple',
'ADA': 'Cardano',
'DOGE': 'Dogecoin',
'AVAX': 'Avalanche',
'DOT': 'Polkadot',
'MATIC': 'Polygon'
}
base_symbol = symbol.split('/')[0] if '/' in symbol else symbol
if base_symbol in crypto_names:
keywords.extend(crypto_names[base_symbol])
# 只添加一个全名,避免大小写重复
keywords.append(crypto_names[base_symbol])
# 添加价格相关关键词
if market == 'Crypto':
keywords.extend(['$100k', '$100000', '100k', 'ETF', 'approval'])
# 优化:移除通用关键词(如 '$100k', 'ETF', 'approval'),这些会匹配到很多不相关的市场
# 只保留与资产直接相关的关键词,最多2-3个
return keywords
# 去重并限制数量(最多3个关键词)
unique_keywords = []
seen = set()
for kw in keywords:
kw_lower = kw.lower()
if kw_lower not in seen:
seen.add(kw_lower)
unique_keywords.append(kw)
if len(unique_keywords) >= 3: # 最多3个关键词
break
logger.info(f"Extracted {len(unique_keywords)} Polymarket keywords (optimized from {len(keywords)}): {unique_keywords}")
return unique_keywords
# 全局实例
@@ -204,12 +204,21 @@ class PendingOrderWorker:
try:
sc = load_strategy_configs(int(sid))
exec_mode = (sc.get("execution_mode") or "").strip().lower()
if exec_mode != "live":
logger.debug(f"[PositionSync] Strategy {sid} skipped: execution_mode='{exec_mode}' (needs 'live')")
# 修改:即使signal模式,如果指定了target_strategy_id(策略启动时调用),也要同步
# 这样可以清理用户在交易所手动平仓但数据库记录还在的"幽灵持仓"
if exec_mode != "live" and not target_strategy_id:
logger.debug(f"[PositionSync] Strategy {sid} skipped: execution_mode='{exec_mode}' (needs 'live' or explicit target)")
continue
sync_user_id = int(sc.get("user_id") or 1)
exchange_config = resolve_exchange_config(sc.get("exchange_config") or {}, user_id=sync_user_id)
safe_cfg = safe_exchange_config_for_log(exchange_config)
# 检查 exchange_id 是否有效,如果为空或无效则跳过同步(signal模式可能没有配置交易所)
exchange_id = str(exchange_config.get("exchange_id") or "").strip().lower()
if not exchange_id:
logger.debug(f"[PositionSync] Strategy {sid} skipped: exchange_id is empty (signal mode or no exchange config)")
continue
market_type = (sc.get("market_type") or exchange_config.get("market_type") or "swap")
market_type = str(market_type or "swap").strip().lower()
if market_type in ("futures", "future", "perp", "perpetual"):
@@ -237,7 +246,12 @@ class PendingOrderWorker:
except ImportError:
pass
client = create_client(exchange_config, market_type=market_type)
# 尝试创建客户端,如果失败则跳过(可能是配置错误)
try:
client = create_client(exchange_config, market_type=market_type)
except Exception as e:
logger.debug(f"[PositionSync] Strategy {sid} skipped: failed to create client (exchange_id={exchange_id}): {e}")
continue
# Build an "exchange snapshot" per symbol+side
exch_size: Dict[str, Dict[str, float]] = {} # {symbol: {long: size, short: size}}
@@ -24,7 +24,7 @@ class PolymarketAnalyzer:
self.data_collector = get_market_data_collector()
self.polymarket_source = PolymarketDataSource()
def analyze_market(self, market_id: str, user_id: int = None, use_cache: bool = True) -> Dict:
def analyze_market(self, market_id: str, user_id: int = None, use_cache: bool = True, language: str = 'zh-CN', model: str = None) -> Dict:
"""
分析单个预测市场
@@ -32,6 +32,7 @@ class PolymarketAnalyzer:
market_id: 市场ID
user_id: 用户ID可选用于用户特定分析
use_cache: 是否使用缓存的分析结果默认True
language: 语言设置'zh-CN' 'en-US'用于生成对应语言的AI分析结果
Returns:
分析结果字典
@@ -64,7 +65,8 @@ class PolymarketAnalyzer:
question=market['question'],
current_market_prob=market['current_probability'],
related_news=related_news,
asset_data=asset_data
asset_data=asset_data,
language=language
)
# 5. 计算机会评分
@@ -193,9 +195,12 @@ class PolymarketAnalyzer:
return []
def _ai_predict_probability(self, question: str, current_market_prob: float,
related_news: List, asset_data: Dict) -> Dict:
related_news: List, asset_data: Dict, language: str = 'zh-CN') -> Dict:
"""使用AI预测事件概率"""
try:
# 根据语言设置构建prompt
is_english = language.lower() in ['en', 'en-us', 'en_us']
# 构建prompt
news_text = "\n".join([f"- {n.get('title', '')[:100]}" for n in related_news[:5]])
@@ -204,7 +209,16 @@ class PolymarketAnalyzer:
price_data = asset_data.get('price', {})
indicators = asset_data.get('indicators', {})
if price_data:
asset_text = f"""
if is_english:
asset_text = f"""
Related Asset Data:
- Current Price: {price_data.get('current_price', 'N/A')}
- 24h Change: {price_data.get('change_24h', 0):.2f}%
- RSI: {indicators.get('rsi', {}).get('value', 'N/A')}
- MACD: {indicators.get('macd', {}).get('signal', 'N/A')}
"""
else:
asset_text = f"""
相关资产数据
- 当前价格: {price_data.get('current_price', 'N/A')}
- 24h涨跌幅: {price_data.get('change_24h', 0):.2f}%
@@ -212,7 +226,38 @@ class PolymarketAnalyzer:
- MACD: {indicators.get('macd', {}).get('signal', 'N/A')}
"""
prompt = f"""分析以下预测市场事件,评估其发生的概率:
if is_english:
prompt = f"""Analyze the following prediction market event and assess its probability of occurrence:
Question: {question}
Current Market Probability: {current_market_prob}%
Related News:
{news_text if news_text else "No related news available"}
{asset_text}
Please analyze based on the following dimensions:
1. Success rate of similar historical events
2. Current news and trends
3. Related asset price movements and technical indicators
4. Macro environment factors (VIX, DXY, interest rates, etc.)
5. Market sentiment indicators
Output JSON format:
{{
"predicted_probability": 72.5, // Your predicted probability (0-100)
"confidence": 75.0, // Confidence level (0-100)
"reasoning": "Detailed analysis...",
"key_factors": ["Factor 1", "Factor 2"],
"risk_factors": ["Risk 1", "Risk 2"]
}}
IMPORTANT: All text in the JSON response (reasoning, key_factors, risk_factors) must be in English."""
system_prompt = "You are a professional market analyst specializing in prediction market analysis. Please objectively assess the probability of events occurring based on the provided data. Respond in English."
else:
prompt = f"""分析以下预测市场事件,评估其发生的概率:
问题{question}
当前市场概率{current_market_prob}%
@@ -236,13 +281,17 @@ class PolymarketAnalyzer:
"reasoning": "详细分析...",
"key_factors": ["因素1", "因素2"],
"risk_factors": ["风险1", "风险2"]
}}"""
}}
重要提示JSON响应中的所有文本reasoningkey_factorsrisk_factors必须使用中文"""
system_prompt = "你是一个专业的市场分析师,擅长分析预测市场事件。请基于提供的数据,客观评估事件发生的概率。请使用中文回答。"
# 调用LLM
messages = [
{
"role": "system",
"content": "你是一个专业的市场分析师,擅长分析预测市场事件。请基于提供的数据,客观评估事件发生的概率。"
"content": system_prompt
},
{
"role": "user",
@@ -500,11 +549,21 @@ class PolymarketAnalyzer:
age = (datetime.now() - created_at.replace(tzinfo=None)).total_seconds() / 60
return age < max_age_minutes
def _save_analysis_to_db(self, analysis: Dict, user_id: int = None):
"""保存分析结果到数据库"""
def _save_analysis_to_db(self, analysis: Dict, user_id: int = None, language: str = 'en-US', model: str = None):
"""
保存分析结果到数据库
Args:
analysis: 分析结果字典
user_id: 用户ID
language: 语言设置
model: 使用的模型
"""
try:
with get_db_connection() as db:
cur = db.cursor()
# 1. 保存到 qd_polymarket_ai_analysis 表(Polymarket专用表)
cur.execute("""
INSERT INTO qd_polymarket_ai_analysis
(market_id, user_id, ai_predicted_probability, market_probability,
@@ -524,8 +583,41 @@ class PolymarketAnalyzer:
json.dumps(analysis.get('key_factors', [])),
analysis.get('related_assets', [])
))
# 2. 同时保存到 qd_analysis_tasks 表(用于管理员统计和统一的历史记录查看)
market_info = analysis.get('market', {})
market_title = market_info.get('question', '') or market_info.get('title', '') or f"Polymarket Market {analysis['market_id']}"
result_json = json.dumps({
'market_id': analysis['market_id'],
'market_title': market_title,
'analysis': analysis,
'market': market_info,
'type': 'polymarket' # 标记为Polymarket分析
}, ensure_ascii=False)
cur.execute("""
INSERT INTO qd_analysis_tasks
(user_id, market, symbol, model, language, status, result_json, error_message, created_at, completed_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
RETURNING id
""", (
int(user_id) if user_id else 1,
'Polymarket', # market字段
str(analysis['market_id']), # symbol字段存储market_id
str(model) if model else '',
str(language),
'completed',
result_json,
''
))
task_row = cur.fetchone()
task_id = task_row['id'] if task_row else None
db.commit()
cur.close()
if task_id:
logger.debug(f"Saved Polymarket analysis to both tables: task_id={task_id}, market_id={analysis['market_id']}")
except Exception as e:
logger.error(f"Failed to save analysis to DB: {e}")
@@ -17,7 +17,7 @@ logger = get_logger(__name__)
class PolymarketWorker:
"""Polymarket数据更新和分析后台任务"""
def __init__(self, update_interval_minutes: int = 30, analysis_cache_minutes: int = 30):
def __init__(self, update_interval_minutes: int = 30, analysis_cache_minutes: int = 1440): # 24小时缓存
"""
初始化后台任务
@@ -112,12 +112,37 @@ class PolymarketWorker:
markets_list = list(unique_markets.values())
logger.info(f"Starting batch analysis for {len(markets_list)} markets...")
# 使用批量分析器,一次性分析所有市场
# AI会筛选出最有交易机会的市场(最多20个)
analyzed_markets = self.batch_analyzer.batch_analyze_markets(
markets_list,
max_opportunities=20
)
# 优化策略:先用规则筛选,只对高价值机会调用LLM
# 这样可以大幅减少LLM调用次数,节省token
# 1. 先用规则筛选出最有价值的机会(不调用LLM)
rule_based_opportunities = []
for market in markets_list:
prob = market.get('current_probability', 50.0)
volume = market.get('volume_24h', 0)
divergence = abs(prob - 50.0)
# 规则筛选:高交易量 + 明显概率偏差
if volume > 5000 and divergence > 8:
rule_based_opportunities.append(market)
# 2. 只对规则筛选出的机会调用LLM(最多30个,节省token)
if rule_based_opportunities:
logger.info(f"Rule-based filtering: {len(rule_based_opportunities)} opportunities, analyzing top 30 with LLM")
# 按交易量和概率偏差排序,取前30个
rule_based_opportunities.sort(
key=lambda x: (x.get('volume_24h', 0) * abs(x.get('current_probability', 50) - 50)),
reverse=True
)
top_opportunities = rule_based_opportunities[:30]
analyzed_markets = self.batch_analyzer.batch_analyze_markets(
top_opportunities,
max_opportunities=30 # 只分析30个最有价值的机会
)
else:
logger.info("No rule-based opportunities found, skipping LLM analysis")
analyzed_markets = []
# 3. 保存分析结果到数据库
if analyzed_markets:
@@ -53,48 +53,32 @@ class TradingExecutor:
self._ensure_db_columns()
def _ensure_db_columns(self):
"""确保必要的数据库字段存在(支持 SQLite 和 PostgreSQL"""
"""确保必要的数据库字段存在(PostgreSQL)"""
try:
db_type = os.getenv('DB_TYPE', 'sqlite').lower()
with get_db_connection() as db:
cursor = db.cursor()
col_names = set()
if db_type == 'postgresql':
# PostgreSQL: 使用 information_schema 查询列
try:
cursor.execute("""
SELECT column_name FROM information_schema.columns
WHERE table_name = 'qd_strategy_positions'
""")
cols = cursor.fetchall() or []
col_names = {c.get('column_name') or c.get('COLUMN_NAME') for c in cols if isinstance(c, dict)}
except Exception:
col_names = set()
else:
# SQLite: 使用 PRAGMA table_info
try:
cursor.execute("PRAGMA table_info(qd_strategy_positions)")
cols = cursor.fetchall() or []
col_names = {c.get('name') for c in cols if isinstance(c, dict)}
except Exception:
col_names = set()
# PostgreSQL: 使用 information_schema 查询列
try:
cursor.execute("""
SELECT column_name FROM information_schema.columns
WHERE table_name = 'qd_strategy_positions'
""")
cols = cursor.fetchall() or []
col_names = {c.get('column_name') or c.get('COLUMN_NAME') for c in cols if isinstance(c, dict)}
except Exception:
col_names = set()
if 'highest_price' not in col_names:
logger.info(f"Adding highest_price column to qd_strategy_positions ({db_type})...")
if db_type == 'postgresql':
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN IF NOT EXISTS highest_price DOUBLE PRECISION DEFAULT 0")
else:
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN highest_price REAL DEFAULT 0")
logger.info("Adding highest_price column to qd_strategy_positions...")
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN IF NOT EXISTS highest_price DOUBLE PRECISION DEFAULT 0")
db.commit()
logger.info("highest_price column added")
if 'lowest_price' not in col_names:
logger.info(f"Adding lowest_price column to qd_strategy_positions ({db_type})...")
if db_type == 'postgresql':
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN IF NOT EXISTS lowest_price DOUBLE PRECISION DEFAULT 0")
else:
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN lowest_price REAL DEFAULT 0")
logger.info("Adding lowest_price column to qd_strategy_positions...")
cursor.execute("ALTER TABLE qd_strategy_positions ADD COLUMN IF NOT EXISTS lowest_price DOUBLE PRECISION DEFAULT 0")
db.commit()
logger.info("lowest_price column added")
@@ -642,10 +626,20 @@ class TradingExecutor:
return
# ============================================
# 启动时:完全依赖本地数据库的持仓状态(虚拟持仓
# 启动时:同步持仓状态,清理"幽灵持仓"
# ============================================
# 信号模式下,不再同步交易所持仓
pass
# 即使信号模式下,也要在启动时检查并清理用户在交易所手动平仓但数据库记录还在的情况
# 这样可以避免策略认为还有持仓而无法执行新的开仓信号
try:
logger.info(f"策略 {strategy_id} 启动时检查持仓同步...")
# 调用持仓同步逻辑(即使signal模式也要检查)
from app import get_pending_order_worker
worker = get_pending_order_worker()
if worker and hasattr(worker, '_sync_positions_best_effort'):
worker._sync_positions_best_effort(target_strategy_id=strategy_id)
logger.info(f"策略 {strategy_id} 启动时持仓同步完成")
except Exception as e:
logger.warning(f"策略 {strategy_id} 启动时持仓同步失败(不影响启动): {e}")
# 获取当前持仓最高价(从本地数据库读取)
current_pos_list = self._get_current_positions(strategy_id, symbol)
@@ -1100,8 +1094,12 @@ class TradingExecutor:
return None
def _is_strategy_running(self, strategy_id: int) -> bool:
"""检查策略是否在运行"""
"""
检查策略是否在运行
同时检查数据库状态和线程状态避免重启后状态不一致
"""
try:
# 1. 检查数据库状态
with get_db_connection() as db:
cursor = db.cursor()
cursor.execute(
@@ -1110,8 +1108,34 @@ class TradingExecutor:
)
result = cursor.fetchone()
cursor.close()
return result and result.get('status') == 'running'
except:
db_status = result and result.get('status') == 'running'
# 2. 检查线程是否真的在运行
with self.lock:
thread = self.running_strategies.get(strategy_id)
thread_running = thread is not None and thread.is_alive()
# 3. 如果数据库状态是running但线程不在运行,说明状态不一致(可能是重启后恢复失败)
if db_status and not thread_running:
logger.warning(f"Strategy {strategy_id} status mismatch: DB=running but thread not running. Updating DB status to stopped.")
# 更新数据库状态为stopped,避免策略"僵尸"状态
try:
with get_db_connection() as db:
cursor = db.cursor()
cursor.execute(
"UPDATE qd_strategies_trading SET status = 'stopped' WHERE id = %s",
(strategy_id,)
)
db.commit()
cursor.close()
except Exception as e:
logger.error(f"Failed to update strategy {strategy_id} status to stopped: {e}")
return False
# 4. 只有数据库状态和线程状态都一致时才返回True
return db_status and thread_running
except Exception as e:
logger.error(f"Error checking strategy {strategy_id} running status: {e}")
return False
def _init_exchange(