@@ -57,7 +57,7 @@ def _extract_indicator_meta_from_code(code: str) -> Dict[str, str]:
|
||||
|
||||
def _row_to_indicator(row: Dict[str, Any], user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Map SQLite row -> frontend expected indicator shape.
|
||||
Map database row -> frontend expected indicator shape.
|
||||
|
||||
Frontend uses:
|
||||
- id, name, description, code
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"""
|
||||
Polymarket预测市场API路由
|
||||
提供预测市场数据和分析接口(只读,不涉及交易)
|
||||
提供按需分析接口(只读,不涉及交易)
|
||||
"""
|
||||
from flask import Blueprint, jsonify, request, g
|
||||
|
||||
from app.utils.auth import login_required
|
||||
from app.utils.logger import get_logger
|
||||
from app.data_sources.polymarket import PolymarketDataSource
|
||||
from app.utils.db import get_db_connection
|
||||
from app.data_sources.polymarket import PolymarketDataSource
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -19,286 +19,208 @@ polymarket_bp = Blueprint('polymarket', __name__)
|
||||
polymarket_source = PolymarketDataSource()
|
||||
|
||||
|
||||
@polymarket_bp.route("/markets", methods=["GET"])
|
||||
@polymarket_bp.route("/analyze", methods=["POST"])
|
||||
@login_required
|
||||
def get_markets():
|
||||
def analyze_polymarket():
|
||||
"""
|
||||
获取预测市场列表
|
||||
分析Polymarket预测市场(用户输入链接或标题)
|
||||
|
||||
Query params:
|
||||
category: crypto/politics/economics/sports (optional)
|
||||
sort_by: volume_24h/ai_score/probability_change (default: volume_24h)
|
||||
limit: 数量 (default: 20)
|
||||
POST /api/polymarket/analyze
|
||||
Body: {
|
||||
"input": "https://polymarket.com/event/xxx" 或 "市场标题",
|
||||
"language": "zh-CN" (optional)
|
||||
}
|
||||
|
||||
流程:
|
||||
1. 从输入中解析market_id或slug
|
||||
2. 从API获取市场数据
|
||||
3. 检查计费并扣除积分
|
||||
4. 调用AI分析
|
||||
5. 返回分析结果
|
||||
"""
|
||||
try:
|
||||
category = request.args.get("category")
|
||||
sort_by = request.args.get("sort_by", "volume_24h")
|
||||
limit = int(request.args.get("limit", 20))
|
||||
from app.services.billing_service import BillingService
|
||||
from app.services.polymarket_analyzer import PolymarketAnalyzer
|
||||
from decimal import Decimal
|
||||
|
||||
# 获取市场列表
|
||||
markets = polymarket_source.get_trending_markets(category, limit * 2)
|
||||
|
||||
logger.info(f"Fetched {len(markets)} markets from PolymarketDataSource (category={category}, limit={limit})")
|
||||
|
||||
if not markets:
|
||||
logger.warning(f"No markets returned from PolymarketDataSource. This may indicate API issues or empty cache.")
|
||||
user_id = getattr(g, 'user_id', None)
|
||||
if not user_id:
|
||||
return jsonify({
|
||||
"code": 1,
|
||||
"msg": "success",
|
||||
"data": [],
|
||||
"warning": "No markets available. API may be unavailable or cache is empty."
|
||||
})
|
||||
"code": 0,
|
||||
"msg": "User not authenticated",
|
||||
"data": None
|
||||
}), 401
|
||||
|
||||
# 从数据库读取缓存的AI分析结果(由后台任务批量分析生成)
|
||||
# 只读取30分钟内的分析结果
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
market_ids = [m.get('market_id') for m in markets if m.get('market_id')]
|
||||
|
||||
if market_ids:
|
||||
# 查询30分钟内的分析结果
|
||||
from datetime import datetime, timedelta
|
||||
cache_cutoff = datetime.now() - timedelta(minutes=30)
|
||||
placeholders = ','.join(['%s'] * len(market_ids))
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT market_id, ai_predicted_probability, market_probability, divergence,
|
||||
recommendation, confidence_score, opportunity_score, reasoning, key_factors
|
||||
FROM qd_polymarket_ai_analysis
|
||||
WHERE market_id IN ({placeholders})
|
||||
AND user_id IS NULL
|
||||
AND created_at > %s
|
||||
ORDER BY opportunity_score DESC
|
||||
""", market_ids + [cache_cutoff])
|
||||
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
# 构建分析结果映射
|
||||
analysis_map = {}
|
||||
for row in rows:
|
||||
market_id = row.get('market_id')
|
||||
if market_id:
|
||||
key_factors_raw = row.get('key_factors')
|
||||
key_factors = []
|
||||
if key_factors_raw:
|
||||
try:
|
||||
if isinstance(key_factors_raw, str):
|
||||
key_factors = json.loads(key_factors_raw)
|
||||
else:
|
||||
key_factors = key_factors_raw if isinstance(key_factors_raw, list) else []
|
||||
except:
|
||||
key_factors = []
|
||||
|
||||
analysis_map[market_id] = {
|
||||
'predicted_probability': float(row.get('ai_predicted_probability') or 0),
|
||||
'recommendation': row.get('recommendation') or 'HOLD',
|
||||
'confidence_score': float(row.get('confidence_score') or 0),
|
||||
'opportunity_score': float(row.get('opportunity_score') or 0),
|
||||
'divergence': float(row.get('divergence') or 0),
|
||||
'reasoning': row.get('reasoning') or '',
|
||||
'key_factors': key_factors
|
||||
}
|
||||
|
||||
# 为每个市场添加分析结果
|
||||
for market in markets:
|
||||
market_id = market.get('market_id')
|
||||
if market_id and market_id in analysis_map:
|
||||
market['ai_analysis'] = analysis_map[market_id]
|
||||
else:
|
||||
market['ai_analysis'] = None
|
||||
data = request.get_json() or {}
|
||||
input_text = (data.get('input') or '').strip()
|
||||
language = data.get('language', 'zh-CN')
|
||||
|
||||
if not input_text:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": "Input is required (Polymarket URL or market title)",
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
# 1. 解析market_id或slug
|
||||
market_id = None
|
||||
slug = None
|
||||
|
||||
# 尝试从URL中提取
|
||||
url_patterns = [
|
||||
r'polymarket\.com/event/([^/?]+)',
|
||||
r'polymarket\.com/markets/(\d+)',
|
||||
r'polymarket\.com/market/(\d+)',
|
||||
]
|
||||
|
||||
for pattern in url_patterns:
|
||||
match = re.search(pattern, input_text)
|
||||
if match:
|
||||
extracted = match.group(1)
|
||||
# 如果是数字,是market_id;否则是slug
|
||||
if extracted.isdigit():
|
||||
market_id = extracted
|
||||
else:
|
||||
# 如果没有市场ID,设置分析为None
|
||||
for market in markets:
|
||||
market['ai_analysis'] = None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cached analysis: {e}")
|
||||
# 出错时,所有市场都没有分析结果
|
||||
for market in markets:
|
||||
market['ai_analysis'] = None
|
||||
slug = extracted
|
||||
break
|
||||
|
||||
# 筛选:优先返回有交易机会的市场,但也包含其他活跃市场
|
||||
# 重点:不是简单复制数据,而是找到交易机会,但也要保证有足够的数据展示
|
||||
opportunity_markets = []
|
||||
other_markets = []
|
||||
# 如果没有从URL提取到,尝试搜索市场
|
||||
if not market_id and not slug:
|
||||
# 尝试通过标题搜索
|
||||
logger.info(f"Searching for market by title: {input_text[:100]}")
|
||||
search_results = polymarket_source.search_markets(input_text, limit=5)
|
||||
if search_results:
|
||||
# 使用第一个搜索结果
|
||||
market_id = search_results[0].get('market_id')
|
||||
logger.info(f"Found market via search: {market_id}")
|
||||
|
||||
for market in markets:
|
||||
ai_analysis = market.get('ai_analysis')
|
||||
volume = market.get('volume_24h', 0) or 0
|
||||
prob = market.get('current_probability', 50.0) or 50.0
|
||||
if not market_id and not slug:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": "Could not parse market ID or slug from input. Please provide a valid Polymarket URL or market title.",
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
# 2. 获取市场数据
|
||||
if market_id:
|
||||
market = polymarket_source.get_market_details(market_id)
|
||||
elif slug:
|
||||
# 通过slug查找市场(需要先搜索)
|
||||
search_results = polymarket_source.search_markets(slug, limit=10)
|
||||
market = None
|
||||
for result in search_results:
|
||||
if result.get('slug') == slug or slug in (result.get('question') or ''):
|
||||
market = result
|
||||
market_id = result.get('market_id')
|
||||
break
|
||||
|
||||
if ai_analysis:
|
||||
opportunity_score = ai_analysis.get('opportunity_score', 0) or 0
|
||||
divergence = abs(ai_analysis.get('divergence', 0) or 0)
|
||||
confidence = ai_analysis.get('confidence_score', 0) or 0
|
||||
|
||||
# 筛选条件:机会评分>60 或 (差异>15% 且 置信度>70) 或 (差异>10% 且 置信度>60)
|
||||
if opportunity_score > 60 or (divergence > 15 and confidence > 70) or (divergence > 10 and confidence > 60):
|
||||
opportunity_markets.append(market)
|
||||
elif volume > 5000: # 交易量较大的也作为备选
|
||||
other_markets.append(market)
|
||||
else:
|
||||
# 如果没有AI分析,但交易量大且概率不是50%(说明有明确的市场共识),也包含
|
||||
if volume > 5000 and abs(prob - 50.0) > 5: # 降低阈值,包含更多市场
|
||||
other_markets.append(market)
|
||||
if not market and search_results:
|
||||
# 使用第一个搜索结果
|
||||
market = search_results[0]
|
||||
market_id = market.get('market_id')
|
||||
|
||||
# 合并结果:优先显示机会市场,然后补充其他活跃市场
|
||||
if opportunity_markets:
|
||||
# 如果有机会市场,优先显示它们,然后补充其他市场直到达到limit
|
||||
result_markets = opportunity_markets[:limit]
|
||||
remaining = limit - len(result_markets)
|
||||
if remaining > 0 and other_markets:
|
||||
result_markets.extend(other_markets[:remaining])
|
||||
opportunity_markets = result_markets
|
||||
elif other_markets:
|
||||
# 如果没有机会市场,至少返回高交易量的市场
|
||||
opportunity_markets = other_markets[:limit]
|
||||
else:
|
||||
# 如果都没有,返回原始市场列表的前几个
|
||||
opportunity_markets = markets[:min(limit, len(markets))]
|
||||
|
||||
# 排序和筛选
|
||||
# 辅助函数:安全获取 ai_analysis 数据(处理 None 情况)
|
||||
def safe_get_ai_analysis(market, key, default=0):
|
||||
ai_analysis = market.get('ai_analysis')
|
||||
if ai_analysis is None:
|
||||
return default
|
||||
return ai_analysis.get(key, default) or default
|
||||
|
||||
if sort_by == "ai_score":
|
||||
# 按AI机会评分排序(高概率/高回报比优先)
|
||||
opportunity_markets.sort(
|
||||
key=lambda x: (
|
||||
safe_get_ai_analysis(x, 'opportunity_score', 0),
|
||||
abs(safe_get_ai_analysis(x, 'divergence', 0)), # 差异越大越好
|
||||
safe_get_ai_analysis(x, 'confidence_score', 0) # 置信度越高越好
|
||||
),
|
||||
reverse=True
|
||||
)
|
||||
elif sort_by == "high_probability":
|
||||
# 高概率机会:AI预测概率 > 市场概率 + 10%
|
||||
opportunity_markets.sort(
|
||||
key=lambda x: (
|
||||
safe_get_ai_analysis(x, 'ai_predicted_probability', 0),
|
||||
safe_get_ai_analysis(x, 'confidence_score', 0)
|
||||
),
|
||||
reverse=True
|
||||
)
|
||||
elif sort_by == "high_return":
|
||||
# 高回报比机会:AI与市场差异大且置信度高
|
||||
opportunity_markets.sort(
|
||||
key=lambda x: (
|
||||
abs(safe_get_ai_analysis(x, 'divergence', 0)) *
|
||||
safe_get_ai_analysis(x, 'confidence_score', 0) / 100,
|
||||
safe_get_ai_analysis(x, 'opportunity_score', 0)
|
||||
),
|
||||
reverse=True
|
||||
)
|
||||
elif sort_by == "probability_change":
|
||||
# 需要历史数据,暂时按volume排序
|
||||
opportunity_markets.sort(key=lambda x: x.get('volume_24h', 0), reverse=True)
|
||||
else:
|
||||
opportunity_markets.sort(key=lambda x: x.get('volume_24h', 0), reverse=True)
|
||||
|
||||
return jsonify({
|
||||
"code": 1,
|
||||
"msg": "success",
|
||||
"data": opportunity_markets[:limit],
|
||||
"total_opportunities": len(opportunity_markets),
|
||||
"total_markets": len(markets)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"get_markets failed: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": str(e),
|
||||
"data": None
|
||||
}), 500
|
||||
|
||||
|
||||
@polymarket_bp.route("/markets/<market_id>", methods=["GET"])
|
||||
@login_required
|
||||
def get_market_detail(market_id: str):
|
||||
"""
|
||||
获取单个市场详情和AI分析
|
||||
|
||||
支持通过market ID或slug查询
|
||||
"""
|
||||
try:
|
||||
# 确保market_id是字符串
|
||||
market_id = str(market_id).strip()
|
||||
# 获取市场数据
|
||||
market = polymarket_source.get_market_details(market_id)
|
||||
if not market:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": "Market not found",
|
||||
"msg": "Market not found. Please check the URL or title.",
|
||||
"data": None
|
||||
}), 404
|
||||
|
||||
# 从数据库读取缓存的AI分析结果(30分钟内)
|
||||
analysis = None
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cache_cutoff = datetime.now() - timedelta(minutes=30)
|
||||
|
||||
cur.execute("""
|
||||
SELECT ai_predicted_probability, market_probability, divergence,
|
||||
recommendation, confidence_score, opportunity_score,
|
||||
reasoning, key_factors, related_assets, created_at
|
||||
FROM qd_polymarket_ai_analysis
|
||||
WHERE market_id = %s AND user_id IS NULL AND created_at > %s
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""", (market_id, cache_cutoff))
|
||||
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
if row:
|
||||
key_factors_raw = row.get('key_factors')
|
||||
key_factors = []
|
||||
if key_factors_raw:
|
||||
try:
|
||||
if isinstance(key_factors_raw, str):
|
||||
key_factors = json.loads(key_factors_raw)
|
||||
else:
|
||||
key_factors = key_factors_raw if isinstance(key_factors_raw, list) else []
|
||||
except:
|
||||
key_factors = []
|
||||
|
||||
analysis = {
|
||||
"ai_predicted_probability": float(row.get('ai_predicted_probability') or 0),
|
||||
"market_probability": float(row.get('market_probability') or 0),
|
||||
"divergence": float(row.get('divergence') or 0),
|
||||
"recommendation": row.get('recommendation') or 'HOLD',
|
||||
"confidence_score": float(row.get('confidence_score') or 0),
|
||||
"opportunity_score": float(row.get('opportunity_score') or 0),
|
||||
"reasoning": row.get('reasoning') or '',
|
||||
"key_factors": key_factors,
|
||||
"related_assets": row.get('related_assets') if row.get('related_assets') else []
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cached analysis for market {market_id}: {e}")
|
||||
if not market_id:
|
||||
market_id = market.get('market_id')
|
||||
|
||||
# 资产交易机会(暂时返回空,可以后续实现)
|
||||
asset_opportunities = []
|
||||
if not market_id:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": "Invalid market data",
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
# 3. 检查计费
|
||||
billing = BillingService()
|
||||
cost = 0
|
||||
|
||||
if billing.is_billing_enabled():
|
||||
cost = billing.get_feature_cost('polymarket_deep_analysis')
|
||||
|
||||
if cost > 0:
|
||||
user_credits = billing.get_user_credits(user_id)
|
||||
if user_credits < Decimal(str(cost)):
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": "Insufficient credits",
|
||||
"data": {
|
||||
"required": cost,
|
||||
"current": float(user_credits),
|
||||
"shortage": float(Decimal(str(cost)) - user_credits)
|
||||
}
|
||||
}), 400
|
||||
|
||||
# 扣除积分(使用check_and_consume方法,它会自动从配置中获取成本)
|
||||
success, error_msg = billing.check_and_consume(
|
||||
user_id=user_id,
|
||||
feature='polymarket_deep_analysis',
|
||||
reference_id=f"polymarket_{market_id}"
|
||||
)
|
||||
|
||||
if not success:
|
||||
# 检查是否是积分不足的错误
|
||||
if error_msg.startswith('insufficient_credits'):
|
||||
parts = error_msg.split(':')
|
||||
if len(parts) >= 3:
|
||||
current_credits = parts[1]
|
||||
required_credits = parts[2]
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": "Insufficient credits",
|
||||
"data": {
|
||||
"required": float(required_credits),
|
||||
"current": float(current_credits),
|
||||
"shortage": float(Decimal(required_credits) - Decimal(current_credits))
|
||||
}
|
||||
}), 400
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": f"Failed to deduct credits: {error_msg}",
|
||||
"data": None
|
||||
}), 500
|
||||
|
||||
# 4. 执行AI分析(传递语言和模型参数)
|
||||
analyzer = PolymarketAnalyzer()
|
||||
model = request.get_json().get('model') # 可选:从请求中获取模型参数
|
||||
analysis_result = analyzer.analyze_market(
|
||||
market_id,
|
||||
user_id=user_id,
|
||||
use_cache=False,
|
||||
language=language,
|
||||
model=model
|
||||
)
|
||||
|
||||
if analysis_result.get('error'):
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": analysis_result.get('error', 'Analysis failed'),
|
||||
"data": None
|
||||
}), 500
|
||||
|
||||
# 5. 获取剩余积分
|
||||
remaining_credits = 0
|
||||
if billing.is_billing_enabled():
|
||||
remaining_credits = float(billing.get_user_credits(user_id))
|
||||
|
||||
return jsonify({
|
||||
"code": 1,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"market": market,
|
||||
"ai_analysis": analysis,
|
||||
"asset_opportunities": asset_opportunities
|
||||
"analysis": analysis_result,
|
||||
"credits_charged": cost,
|
||||
"remaining_credits": remaining_credits
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"get_market_detail failed: {e}", exc_info=True)
|
||||
logger.error(f"Polymarket analyze API failed: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": str(e),
|
||||
@@ -306,142 +228,99 @@ def get_market_detail(market_id: str):
|
||||
}), 500
|
||||
|
||||
|
||||
@polymarket_bp.route("/markets/<market_id>/opportunities", methods=["GET"])
|
||||
@polymarket_bp.route("/history", methods=["GET"])
|
||||
@login_required
|
||||
def get_market_opportunities(market_id: str):
|
||||
"""获取基于该预测市场的资产交易机会(暂时返回空,后续可扩展)"""
|
||||
try:
|
||||
# 暂时返回空列表,可以后续实现基于预测市场的资产推荐
|
||||
opportunities = []
|
||||
|
||||
return jsonify({
|
||||
"code": 1,
|
||||
"msg": "success",
|
||||
"data": opportunities
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"get_market_opportunities failed: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": str(e),
|
||||
"data": None
|
||||
}), 500
|
||||
|
||||
|
||||
@polymarket_bp.route("/recommendations", methods=["GET"])
|
||||
@login_required
|
||||
def get_recommendations():
|
||||
def get_polymarket_history():
|
||||
"""
|
||||
获取AI推荐的高价值预测市场
|
||||
Get user's Polymarket analysis history.
|
||||
|
||||
Query params:
|
||||
limit: 数量 (default: 10)
|
||||
GET /api/polymarket/history?page=1&page_size=20
|
||||
"""
|
||||
try:
|
||||
limit = int(request.args.get("limit", 10))
|
||||
user_id = g.user_id
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = min(request.args.get('page_size', 20, type=int), 100)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# 获取所有活跃市场
|
||||
all_markets = polymarket_source.get_trending_markets(limit=100)
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
|
||||
# 获取总数
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) AS total
|
||||
FROM qd_analysis_tasks
|
||||
WHERE user_id = %s AND market = 'Polymarket'
|
||||
""", (user_id,))
|
||||
total_row = cur.fetchone()
|
||||
total = total_row['total'] if total_row else 0
|
||||
|
||||
# 获取历史记录
|
||||
cur.execute("""
|
||||
SELECT
|
||||
t.id,
|
||||
t.symbol AS market_id,
|
||||
t.model,
|
||||
t.language,
|
||||
t.status,
|
||||
t.created_at,
|
||||
t.completed_at,
|
||||
t.result_json
|
||||
FROM qd_analysis_tasks t
|
||||
WHERE t.user_id = %s AND t.market = 'Polymarket'
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""", (user_id, page_size, offset))
|
||||
rows = cur.fetchall() or []
|
||||
cur.close()
|
||||
|
||||
# 从数据库读取缓存的AI分析结果(30分钟内,按机会评分排序)
|
||||
recommendations = []
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cache_cutoff = datetime.now() - timedelta(minutes=30)
|
||||
market_ids = [m.get('market_id') for m in all_markets if m.get('market_id')]
|
||||
|
||||
if market_ids:
|
||||
placeholders = ','.join(['%s'] * len(market_ids))
|
||||
cur.execute(f"""
|
||||
SELECT a.market_id, a.opportunity_score, a.recommendation,
|
||||
a.confidence_score, a.reasoning, a.key_factors,
|
||||
m.question, m.current_probability, m.volume_24h, m.category
|
||||
FROM qd_polymarket_ai_analysis a
|
||||
JOIN qd_polymarket_markets m ON a.market_id = m.market_id
|
||||
WHERE a.market_id IN ({placeholders})
|
||||
AND a.user_id IS NULL
|
||||
AND a.created_at > %s
|
||||
AND a.opportunity_score > 70
|
||||
ORDER BY a.opportunity_score DESC
|
||||
LIMIT %s
|
||||
""", market_ids + [cache_cutoff, limit])
|
||||
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
for row in rows:
|
||||
key_factors_raw = row.get('key_factors')
|
||||
key_factors = []
|
||||
if key_factors_raw:
|
||||
try:
|
||||
if isinstance(key_factors_raw, str):
|
||||
key_factors = json.loads(key_factors_raw)
|
||||
else:
|
||||
key_factors = key_factors_raw if isinstance(key_factors_raw, list) else []
|
||||
except:
|
||||
key_factors = []
|
||||
|
||||
recommendations.append({
|
||||
"market_id": row.get('market_id'),
|
||||
"question": row.get('question'),
|
||||
"current_probability": float(row.get('current_probability') or 0),
|
||||
"volume_24h": float(row.get('volume_24h') or 0),
|
||||
"category": row.get('category'),
|
||||
"ai_analysis": {
|
||||
"opportunity_score": float(row.get('opportunity_score') or 0),
|
||||
"recommendation": row.get('recommendation') or 'HOLD',
|
||||
"confidence_score": float(row.get('confidence_score') or 0),
|
||||
"reasoning": row.get('reasoning') or '',
|
||||
"key_factors": key_factors
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load recommendations: {e}")
|
||||
|
||||
# 如果没有缓存结果,返回空列表(等待后台任务分析)
|
||||
# 解析结果
|
||||
items = []
|
||||
for row in rows:
|
||||
result_json = row.get('result_json', '{}')
|
||||
try:
|
||||
result_data = json.loads(result_json) if result_json else {}
|
||||
except:
|
||||
result_data = {}
|
||||
|
||||
market_data = result_data.get('market', {})
|
||||
analysis_data = result_data.get('analysis', {})
|
||||
|
||||
created_at = row.get('created_at')
|
||||
completed_at = row.get('completed_at')
|
||||
if created_at and hasattr(created_at, 'isoformat'):
|
||||
created_at = created_at.isoformat()
|
||||
if completed_at and hasattr(completed_at, 'isoformat'):
|
||||
completed_at = completed_at.isoformat()
|
||||
|
||||
items.append({
|
||||
'id': row.get('id'),
|
||||
'market_id': row.get('market_id'),
|
||||
'market_title': market_data.get('question') or market_data.get('title') or f"Market {row.get('market_id')}",
|
||||
'market_url': market_data.get('polymarket_url'),
|
||||
'ai_predicted_probability': analysis_data.get('ai_predicted_probability'),
|
||||
'market_probability': analysis_data.get('market_probability'),
|
||||
'recommendation': analysis_data.get('recommendation'),
|
||||
'opportunity_score': analysis_data.get('opportunity_score'),
|
||||
'confidence_score': analysis_data.get('confidence_score'),
|
||||
'status': row.get('status'),
|
||||
'created_at': created_at,
|
||||
'completed_at': completed_at
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"code": 1,
|
||||
"msg": "success",
|
||||
"data": recommendations[:limit]
|
||||
"data": {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"get_recommendations failed: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": str(e),
|
||||
"data": None
|
||||
}), 500
|
||||
|
||||
|
||||
@polymarket_bp.route("/search", methods=["GET"])
|
||||
@login_required
|
||||
def search_markets():
|
||||
"""搜索预测市场"""
|
||||
try:
|
||||
keyword = request.args.get("q", "").strip()
|
||||
if not keyword:
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": "Keyword required",
|
||||
"data": None
|
||||
}), 400
|
||||
|
||||
limit = int(request.args.get("limit", 20))
|
||||
|
||||
markets = polymarket_source.search_markets(keyword, limit)
|
||||
|
||||
return jsonify({
|
||||
"code": 1,
|
||||
"msg": "success",
|
||||
"data": markets
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"search_markets failed: {e}", exc_info=True)
|
||||
logger.error(f"Get Polymarket history failed: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
"code": 0,
|
||||
"msg": str(e),
|
||||
|
||||
@@ -1334,32 +1334,29 @@ def get_admin_ai_stats():
|
||||
memory_summary = {}
|
||||
|
||||
# --- Per-user stats ---
|
||||
user_conditions = []
|
||||
# Build WHERE clause for user search (applied after JOIN)
|
||||
user_where_clause = ""
|
||||
user_params = []
|
||||
if search:
|
||||
user_conditions.append("(u.username ILIKE ? OR u.nickname ILIKE ? OR u.email ILIKE ?)")
|
||||
like_val = f"%{search}%"
|
||||
user_params.extend([like_val, like_val, like_val])
|
||||
user_where_clause = "WHERE (u.username ILIKE ? OR u.nickname ILIKE ? OR u.email ILIKE ?)"
|
||||
like_val = f"%{search.strip()}%"
|
||||
user_params = [like_val, like_val, like_val]
|
||||
|
||||
user_where = ""
|
||||
if user_conditions:
|
||||
user_where = "WHERE " + " AND ".join(user_conditions)
|
||||
|
||||
# Count distinct users who have analysis records
|
||||
cur.execute(
|
||||
f"""
|
||||
# Count distinct users who have analysis records (matching search criteria)
|
||||
count_sql = f"""
|
||||
SELECT COUNT(DISTINCT t.user_id) AS cnt
|
||||
FROM qd_analysis_tasks t
|
||||
LEFT JOIN qd_users u ON u.id = t.user_id
|
||||
{user_where}
|
||||
""",
|
||||
tuple(user_params)
|
||||
)
|
||||
user_total = cur.fetchone()['cnt']
|
||||
{user_where_clause}
|
||||
"""
|
||||
cur.execute(count_sql, tuple(user_params))
|
||||
count_result = cur.fetchone()
|
||||
user_total = count_result['cnt'] if count_result else 0
|
||||
|
||||
# Get per-user aggregated stats
|
||||
cur.execute(
|
||||
f"""
|
||||
# Important: Filter by user search criteria AFTER grouping, but we need to apply it in WHERE
|
||||
# Since we're grouping by user fields, we need to filter before GROUP BY
|
||||
stats_sql = f"""
|
||||
SELECT
|
||||
t.user_id,
|
||||
u.username,
|
||||
@@ -1372,13 +1369,12 @@ def get_admin_ai_stats():
|
||||
MIN(t.created_at) AS first_analysis_at
|
||||
FROM qd_analysis_tasks t
|
||||
LEFT JOIN qd_users u ON u.id = t.user_id
|
||||
{user_where}
|
||||
{user_where_clause}
|
||||
GROUP BY t.user_id, u.username, u.nickname, u.email
|
||||
ORDER BY analysis_count DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""",
|
||||
tuple(user_params) + (page_size, offset)
|
||||
)
|
||||
"""
|
||||
cur.execute(stats_sql, tuple(user_params) + (page_size, offset))
|
||||
user_rows = cur.fetchall() or []
|
||||
|
||||
# Get per-user analysis_memory stats (correct/helpful counts)
|
||||
@@ -1417,13 +1413,15 @@ def get_admin_ai_stats():
|
||||
memory_stats_map = {}
|
||||
|
||||
# Get recent analysis records (last 50)
|
||||
# Ensure we get user info even if user_id is NULL or user doesn't exist
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
t.id,
|
||||
t.user_id,
|
||||
u.username,
|
||||
u.nickname,
|
||||
COALESCE(u.username, '') AS username,
|
||||
COALESCE(u.nickname, '') AS nickname,
|
||||
COALESCE(u.email, '') AS email,
|
||||
t.market,
|
||||
t.symbol,
|
||||
t.model,
|
||||
@@ -1432,6 +1430,7 @@ def get_admin_ai_stats():
|
||||
t.completed_at
|
||||
FROM qd_analysis_tasks t
|
||||
LEFT JOIN qd_users u ON u.id = t.user_id
|
||||
WHERE t.user_id IS NOT NULL
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT 50
|
||||
"""
|
||||
@@ -1444,26 +1443,40 @@ def get_admin_ai_stats():
|
||||
user_items = []
|
||||
for row in user_rows:
|
||||
uid = row.get('user_id')
|
||||
if not uid: # Skip rows with NULL user_id
|
||||
continue
|
||||
|
||||
ms = memory_stats_map.get(uid, {})
|
||||
last_at = row.get('last_analysis_at')
|
||||
first_at = row.get('first_analysis_at')
|
||||
if hasattr(last_at, 'isoformat'):
|
||||
|
||||
# Convert datetime to ISO format string if needed
|
||||
if last_at and hasattr(last_at, 'isoformat'):
|
||||
last_at = last_at.isoformat()
|
||||
if hasattr(first_at, 'isoformat'):
|
||||
elif last_at:
|
||||
last_at = str(last_at)
|
||||
else:
|
||||
last_at = None
|
||||
|
||||
if first_at and hasattr(first_at, 'isoformat'):
|
||||
first_at = first_at.isoformat()
|
||||
elif first_at:
|
||||
first_at = str(first_at)
|
||||
else:
|
||||
first_at = None
|
||||
|
||||
user_items.append({
|
||||
'user_id': uid,
|
||||
'username': row.get('username') or '',
|
||||
'nickname': row.get('nickname') or '',
|
||||
'email': row.get('email') or '',
|
||||
'analysis_count': row.get('analysis_count') or 0,
|
||||
'symbol_count': row.get('symbol_count') or 0,
|
||||
'market_count': row.get('market_count') or 0,
|
||||
'correct': ms.get('correct', 0),
|
||||
'incorrect': ms.get('incorrect', 0),
|
||||
'helpful': ms.get('helpful', 0),
|
||||
'not_helpful': ms.get('not_helpful', 0),
|
||||
'user_id': int(uid),
|
||||
'username': str(row.get('username') or ''),
|
||||
'nickname': str(row.get('nickname') or ''),
|
||||
'email': str(row.get('email') or ''),
|
||||
'analysis_count': int(row.get('analysis_count') or 0),
|
||||
'symbol_count': int(row.get('symbol_count') or 0),
|
||||
'market_count': int(row.get('market_count') or 0),
|
||||
'correct': int(ms.get('correct', 0)),
|
||||
'incorrect': int(ms.get('incorrect', 0)),
|
||||
'helpful': int(ms.get('helpful', 0)),
|
||||
'not_helpful': int(ms.get('not_helpful', 0)),
|
||||
'last_analysis_at': last_at,
|
||||
'first_analysis_at': first_at
|
||||
})
|
||||
@@ -1471,22 +1484,38 @@ def get_admin_ai_stats():
|
||||
# Build recent records
|
||||
recent_items = []
|
||||
for row in recent_rows:
|
||||
user_id = row.get('user_id')
|
||||
if not user_id: # Skip rows with NULL user_id
|
||||
continue
|
||||
|
||||
created_at = row.get('created_at')
|
||||
completed_at = row.get('completed_at')
|
||||
if hasattr(created_at, 'isoformat'):
|
||||
|
||||
# Convert datetime to ISO format string if needed
|
||||
if created_at and hasattr(created_at, 'isoformat'):
|
||||
created_at = created_at.isoformat()
|
||||
if hasattr(completed_at, 'isoformat'):
|
||||
elif created_at:
|
||||
created_at = str(created_at)
|
||||
else:
|
||||
created_at = None
|
||||
|
||||
if completed_at and hasattr(completed_at, 'isoformat'):
|
||||
completed_at = completed_at.isoformat()
|
||||
elif completed_at:
|
||||
completed_at = str(completed_at)
|
||||
else:
|
||||
completed_at = None
|
||||
|
||||
recent_items.append({
|
||||
'id': row['id'],
|
||||
'user_id': row.get('user_id'),
|
||||
'username': row.get('username') or '',
|
||||
'nickname': row.get('nickname') or '',
|
||||
'market': row.get('market') or '',
|
||||
'symbol': row.get('symbol') or '',
|
||||
'model': row.get('model') or '',
|
||||
'status': row.get('status') or '',
|
||||
'id': int(row.get('id') or 0),
|
||||
'user_id': int(user_id),
|
||||
'username': str(row.get('username') or ''),
|
||||
'nickname': str(row.get('nickname') or ''),
|
||||
'email': str(row.get('email') or ''),
|
||||
'market': str(row.get('market') or ''),
|
||||
'symbol': str(row.get('symbol') or ''),
|
||||
'model': str(row.get('model') or ''),
|
||||
'status': str(row.get('status') or ''),
|
||||
'created_at': created_at,
|
||||
'completed_at': completed_at
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user