From ca88c11e5f31a2bdc57ac14d98ee594df4ad4736 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 5 Dec 2025 16:44:30 +0800 Subject: [PATCH] update pay ui --- commit.sh | 0 compress-images.sh | 80 -- concept_api.py | 1562 ------------------------------------ concept_api_openapi.json | 1096 ------------------------- concept_hierarchy.json | 1176 --------------------------- src/views/Concept/index.js | 3 +- 6 files changed, 1 insertion(+), 3916 deletions(-) delete mode 100644 commit.sh delete mode 100755 compress-images.sh delete mode 100644 concept_api.py delete mode 100644 concept_api_openapi.json delete mode 100644 concept_hierarchy.json diff --git a/commit.sh b/commit.sh deleted file mode 100644 index e69de29b..00000000 diff --git a/compress-images.sh b/compress-images.sh deleted file mode 100755 index 904ecbc0..00000000 --- a/compress-images.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash - -# 需要压缩的大图片列表 -IMAGES=( - "CoverImage.png" - "BasicImage.png" - "teams-image.png" - "hand-background.png" - "basic-auth.png" - "BgMusicCard.png" - "Landing2.png" - "Landing3.png" - "Landing1.png" - "smart-home.png" - "automotive-background-card.png" -) - -IMG_DIR="src/assets/img" -BACKUP_DIR="$IMG_DIR/original-backup" - -echo "🎨 开始优化图片..." -echo "================================" - -total_before=0 -total_after=0 - -for img in "${IMAGES[@]}"; do - src_path="$IMG_DIR/$img" - - if [ ! -f "$src_path" ]; then - echo "⚠️ 跳过: $img (文件不存在)" - continue - fi - - # 备份原图 - cp "$src_path" "$BACKUP_DIR/$img" - - # 获取原始大小 - before=$(stat -f%z "$src_path" 2>/dev/null || stat -c%s "$src_path" 2>/dev/null) - before_kb=$((before / 1024)) - total_before=$((total_before + before)) - - # 使用sips压缩图片 (降低质量到75, 减少分辨率如果太大) - # 获取图片尺寸 - width=$(sips -g pixelWidth "$src_path" | grep "pixelWidth:" | awk '{print $2}') - - # 如果宽度大于2000px,缩小到2000px - if [ "$width" -gt 2000 ]; then - sips -Z 2000 "$src_path" > /dev/null 2>&1 - fi - - # 获取压缩后大小 - after=$(stat -f%z "$src_path" 2>/dev/null || stat -c%s "$src_path" 2>/dev/null) - after_kb=$((after / 1024)) - total_after=$((total_after + after)) - - # 计算节省 - saved=$((before - after)) - saved_kb=$((saved / 1024)) - percent=$((100 - (after * 100 / before))) - - echo "✅ $img" - echo " ${before_kb} KB → ${after_kb} KB (⬇️ ${saved_kb} KB, -${percent}%)" -done - -echo "" -echo "================================" -echo "📊 总计优化:" -total_before_mb=$((total_before / 1024 / 1024)) -total_after_mb=$((total_after / 1024 / 1024)) -total_saved=$((total_before - total_after)) -total_saved_mb=$((total_saved / 1024 / 1024)) -total_percent=$((100 - (total_after * 100 / total_before))) - -echo " 优化前: ${total_before_mb} MB" -echo " 优化后: ${total_after_mb} MB" -echo " 节省: ${total_saved_mb} MB (-${total_percent}%)" -echo "" -echo "✅ 图片优化完成!" -echo "📁 原始文件已备份到: $BACKUP_DIR" diff --git a/concept_api.py b/concept_api.py deleted file mode 100644 index 1370346d..00000000 --- a/concept_api.py +++ /dev/null @@ -1,1562 +0,0 @@ -import json -import openai -from typing import List, Dict, Optional, Union, Any -from fastapi import FastAPI, HTTPException, Query -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel, Field -from elasticsearch import Elasticsearch -from datetime import datetime, date -import logging -import re -from contextlib import asynccontextmanager -import aiomysql -from decimal import Decimal - -# 配置日志 -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# 全局变量 -es_client = None -openai_client = None -mysql_pool = None - -# 配置 -ES_HOST = 'http://127.0.0.1:9200' -OPENAI_BASE_URL = "http://127.0.0.1:8000/v1" -OPENAI_API_KEY = "dummy" -EMBEDDING_MODEL = "qwen3-embedding-8b" -INDEX_NAME = 'concept_library' - -# MySQL配置 -MYSQL_CONFIG = { - 'host': '192.168.1.8', - 'user': 'root', - 'password': 'Zzl5588161!', - 'db': 'stock', - 'charset': 'utf8mb4', - 'autocommit': True, - 'minsize': 1, - 'maxsize': 10 -} - - -# API生命周期管理 -@asynccontextmanager -async def lifespan(app: FastAPI): - # 启动时初始化 - global es_client, openai_client, mysql_pool - - # 初始化Elasticsearch客户端 - 增加超时配置 - es_client = Elasticsearch( - [ES_HOST], - timeout=30, - max_retries=3, - retry_on_timeout=True - ) - logger.info(f"Connected to Elasticsearch at {ES_HOST}") - - # 初始化OpenAI客户端 - openai_client = openai.OpenAI( - api_key=OPENAI_API_KEY, - base_url=OPENAI_BASE_URL, - timeout=60, - ) - logger.info(f"Initialized OpenAI client") - - # 初始化MySQL连接池 - try: - mysql_pool = await aiomysql.create_pool(**MYSQL_CONFIG) - logger.info(f"Connected to MySQL at {MYSQL_CONFIG['host']}") - except Exception as e: - logger.error(f"Failed to connect to MySQL: {e}") - - yield - - # 关闭时清理资源 - if es_client: - es_client.close() - if mysql_pool: - mysql_pool.close() - await mysql_pool.wait_closed() - logger.info("Cleanup completed") - - -# 创建FastAPI应用 -app = FastAPI( - title="概念搜索API", - description="支持语义和关键词混合搜索的概念库API,包含概念涨跌幅数据", - version="1.2.0", - lifespan=lifespan -) - - -# 添加CORS中间件 -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -# 请求和响应模型 -class SearchRequest(BaseModel): - query: str = Field(..., description="搜索查询文本") - size: int = Field(10, ge=1, le=100, description="每页返回结果数量") - page: int = Field(1, ge=1, description="页码") - search_size: int = Field(100, ge=10, le=1000, description="搜索数量(从ES获取的结果数),用于排序后分页") - semantic_weight: Optional[float] = Field(None, ge=0.0, le=1.0, description="语义搜索权重(0-1),None表示自动计算") - filter_stocks: Optional[List[str]] = Field(None, description="过滤特定股票代码或名称") - trade_date: Optional[date] = Field(None, description="交易日期,格式:YYYY-MM-DD,默认返回最新日期数据") - sort_by: str = Field("change_pct", description="排序方式: change_pct, _score, stock_count, concept_name, added_date") - use_knn: bool = Field(True, description="是否使用KNN搜索优化语义搜索") - - -class StockInfo(BaseModel): - stock_name: str - stock_code: str - reason: Optional[str] = None - industry: Optional[str] = Field(None, alias="行业") - project: Optional[str] = Field(None, alias="项目") - - class Config: - populate_by_name = True - - -class ConceptPriceInfo(BaseModel): - trade_date: date - avg_change_pct: Optional[float] = Field(None, description="平均涨跌幅(%)") - - -class ConceptResult(BaseModel): - concept_id: str - concept: str - description: Optional[str] - stocks: List[StockInfo] - stock_count: int - happened_times: Optional[List[str]] = None - score: float - match_type: str # "semantic", "keyword", "hybrid", "semantic_knn", "hybrid_knn" - highlights: Optional[Dict[str, List[str]]] = None - price_info: Optional[ConceptPriceInfo] = None - - -class SearchResponse(BaseModel): - total: int - took_ms: int - results: List[ConceptResult] - search_info: Dict[str, Any] - price_date: Optional[date] = None - page: int = Field(1, description="当前页码") - total_pages: int = Field(1, description="总页数") - - -class StockConceptInfo(BaseModel): - concept_id: str - concept: str - stock_count: int - happened_times: Optional[List[str]] = None - description: Optional[str] = None - stock_detail: Optional[Dict[str, Any]] = None - price_info: Optional[ConceptPriceInfo] = None - - -class StockConceptsResponse(BaseModel): - stock_code: str - stats: Dict[str, Any] - concepts: List[StockConceptInfo] - price_date: Optional[date] = None - - -class StockSearchResult(BaseModel): - stock_code: str - stock_name: str - concept_count: int - - -class StockSearchResponse(BaseModel): - total: int - stocks: List[StockSearchResult] - - -class PriceTimeSeriesItem(BaseModel): - trade_date: date - avg_change_pct: Optional[float] = Field(None, description="平均涨跌幅(%)") - stock_count: Optional[int] = Field(None, description="当日股票数量") - - -class PriceTimeSeriesResponse(BaseModel): - concept_id: str - concept_name: str - start_date: date - end_date: date - data_points: int - timeseries: List[PriceTimeSeriesItem] - - -# 辅助函数 -async def get_concept_price_data(concept_ids: List[str], trade_date: Optional[date] = None) -> Dict[ - str, ConceptPriceInfo]: - """获取概念的涨跌幅数据""" - if not mysql_pool or not concept_ids: - return {} - - try: - async with mysql_pool.acquire() as conn: - async with conn.cursor(aiomysql.DictCursor) as cursor: - placeholders = ','.join(['%s'] * len(concept_ids)) - - if trade_date: - query = f""" - SELECT concept_id, concept_name, trade_date, avg_change_pct - FROM concept_daily_stats - WHERE concept_id IN ({placeholders}) AND trade_date = %s - """ - await cursor.execute(query, (*concept_ids, trade_date)) - else: - query = f""" - SELECT cds.concept_id, cds.concept_name, cds.trade_date, cds.avg_change_pct - FROM concept_daily_stats cds - INNER JOIN ( - SELECT concept_id, MAX(trade_date) as max_date - FROM concept_daily_stats - WHERE concept_id IN ({placeholders}) - GROUP BY concept_id - ) latest ON cds.concept_id = latest.concept_id - AND cds.trade_date = latest.max_date - """ - await cursor.execute(query, concept_ids) - - rows = await cursor.fetchall() - - result = {} - for row in rows: - result[row['concept_id']] = ConceptPriceInfo( - trade_date=row['trade_date'], - avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] is not None else None - ) - - return result - except Exception as e: - logger.error(f"Error fetching concept price data: {e}") - return {} - - -async def get_latest_trade_date() -> Optional[date]: - """获取最新的交易日期""" - if not mysql_pool: - return None - - try: - async with mysql_pool.acquire() as conn: - async with conn.cursor() as cursor: - query = "SELECT MAX(trade_date) as latest_date FROM concept_daily_stats" - await cursor.execute(query) - result = await cursor.fetchone() - return result[0] if result and result[0] else None - except Exception as e: - logger.error(f"Error fetching latest trade date: {e}") - return None - - -def generate_embedding(text: str) -> List[float]: - """生成文本向量""" - try: - if not text or len(text.strip()) == 0: - logger.warning("Empty text provided for embedding generation") - return [] - - # 限制文本长度 - text = text[:16000] if len(text) > 16000 else text - - # 检查OpenAI客户端是否已初始化 - if not openai_client: - logger.warning("OpenAI client not initialized, falling back to keyword search") - return [] - - response = openai_client.embeddings.create( - model=EMBEDDING_MODEL, - input=[text] - ) - - embedding = response.data[0].embedding - - # 验证embedding是否有效 - if not embedding or len(embedding) == 0: - logger.warning("Empty embedding returned from OpenAI API") - return [] - - logger.debug(f"Successfully generated embedding for text: {text[:100]}...") - return embedding - - except openai.OpenAIError as e: - logger.warning(f"OpenAI API error during embedding generation: {e}. Falling back to keyword search.") - return [] - except Exception as e: - logger.warning(f"Unexpected error during embedding generation: {e}. Falling back to keyword search.") - return [] - - -def calculate_semantic_weight(query: str) -> float: - """根据查询长度动态计算语义权重""" - base_weight = 0.3 - query_length = len(query) - word_count = len(query.split()) - - if query_length < 10: - length_factor = 0.0 - elif query_length < 50: - length_factor = 0.2 - elif query_length < 200: - length_factor = 0.4 - elif query_length < 500: - length_factor = 0.5 - else: - length_factor = 0.6 - - if word_count < 3: - word_factor = 0.0 - elif word_count < 10: - word_factor = 0.2 - elif word_count < 30: - word_factor = 0.3 - else: - word_factor = 0.4 - - if re.search(r'\b\d{6}\b', query): - pattern_factor = -0.2 - elif word_count <= 2 and query_length < 20: - pattern_factor = -0.1 - elif '。' in query or ',' in query or len(query) > 100: - pattern_factor = 0.2 - else: - pattern_factor = 0.0 - - semantic_weight = base_weight + max(length_factor, word_factor) + pattern_factor - semantic_weight = max(0.3, min(0.9, semantic_weight)) - - return semantic_weight - - -def extract_stock_filter(query: str) -> tuple[str, List[str]]: - """从查询中提取股票过滤条件""" - stock_patterns = [] - cleaned_query = query - - code_matches = re.findall(r'\b(\d{6})\b', query) - stock_patterns.extend(code_matches) - - stock_name_pattern = r'([\u4e00-\u9fa5]{2,6})(?:股票|股份|集团|公司)' - name_matches = re.findall(stock_name_pattern, query) - stock_patterns.extend(name_matches) - - for pattern in stock_patterns: - cleaned_query = cleaned_query.replace(pattern, '') - - cleaned_query = ' '.join(cleaned_query.split()) - - return cleaned_query, stock_patterns - - -def build_keyword_query(query: str, stock_filters: List[str] = None) -> Dict: - """构建关键词查询""" - must_queries = [] - - if query.strip(): - must_queries.append({ - "multi_match": { - "query": query, - "fields": [ - "concept^3", - "description^2", - "stocks_reason", - "stocks_full_text", - "stocks.stock_name^2", - "stocks.stock_code^2", - "stocks.项目", - "stocks.reason", - "stocks.行业" - ], - "type": "best_fields", - "analyzer": "ik_smart" - } - }) - - if stock_filters: - stock_query = { - "nested": { - "path": "stocks", - "query": { - "bool": { - "should": [ - {"terms": {"stocks.stock_name": stock_filters}}, - {"terms": {"stocks.stock_code": stock_filters}} - ] - } - } - } - } - must_queries.append(stock_query) - - return { - "bool": { - "must": must_queries - } - } - - -def build_semantic_query(embedding: List[float]) -> Dict: - """构建语义查询""" - return { - "script_score": { - "query": {"match_all": {}}, - "script": { - "source": "cosineSimilarity(params.query_vector, 'description_embedding') + 1.0", - "params": { - "query_vector": embedding - } - } - } - } - - -def build_hybrid_query(query: str, embedding: List[float], semantic_weight: float, - stock_filters: List[str] = None) -> Dict: - """构建混合查询""" - keyword_weight = 1.0 - semantic_weight - queries = [] - - if keyword_weight > 0: - keyword_query = build_keyword_query(query, stock_filters) - queries.append({ - "bool": { - "must": [keyword_query], - "boost": keyword_weight - } - }) - - if semantic_weight > 0 and embedding: - queries.append({ - "script_score": { - "query": {"match_all": {}}, - "script": { - "source": f"cosineSimilarity(params.query_vector, 'description_embedding') * {semantic_weight} + 0.0", - "params": { - "query_vector": embedding - } - }, - "boost": 1.0 - } - }) - - return { - "bool": { - "should": queries, - "minimum_should_match": 1 - } - } - - -def build_hybrid_knn_query( - query: str, - embedding: List[float], - semantic_weight: float, - stock_filters: List[str] = None, - k: int = 100 -) -> Dict: - """构建混合查询(KNN + 关键词)""" - keyword_weight = 1.0 - semantic_weight - - filter_query = None - if stock_filters: - filter_query = { - "nested": { - "path": "stocks", - "query": { - "bool": { - "should": [ - {"terms": {"stocks.stock_name": stock_filters}}, - {"terms": {"stocks.stock_code": stock_filters}} - ] - } - } - } - } - - search_body = { - "knn": { - "field": "description_embedding", - "query_vector": embedding, - "k": k, - "num_candidates": max(k + 50, min(k * 2, 10000)), # 确保 num_candidates > k,最大 10000 - "boost": semantic_weight - } - } - - if filter_query: - search_body["knn"]["filter"] = filter_query - - keyword_query = build_keyword_query(query, stock_filters) - search_body["query"] = { - "bool": { - "must": [keyword_query], - "boost": keyword_weight - } - } - - return search_body - - -# API端点 -@app.get("/", tags=["Health"]) -async def root(): - """健康检查端点""" - return {"status": "healthy", "service": "概念搜索API", "version": "1.2.0"} - - -@app.post("/search", response_model=SearchResponse, tags=["Search"]) -async def search_concepts(request: SearchRequest): - """ - 搜索概念库 - 支持KNN优化的语义搜索 - - 新特性: - - 使用KNN搜索优化语义搜索性能 - - search_size参数控制搜索数量,提高排序灵活性 - - 支持混合搜索(KNN + 关键词) - """ - start_time = datetime.now() - - try: - # 提取股票过滤条件 - cleaned_query, stock_filters = extract_stock_filter(request.query) - if request.filter_stocks: - stock_filters.extend(request.filter_stocks) - - # 计算语义权重 - if request.semantic_weight is not None: - semantic_weight = request.semantic_weight - else: - semantic_weight = calculate_semantic_weight(request.query) - - # 生成embedding(如果需要) - embedding = [] - if semantic_weight > 0: - embedding = generate_embedding(request.query) - if not embedding: - # 已经在generate_embedding中记录了详细日志,这里只调整语义权重 - semantic_weight = 0 - - # 【关键修改】:如果按涨跌幅或添加日期排序,需要获取更多结果 - effective_search_size = request.search_size - if request.sort_by in ["change_pct", "added_date"]: - # 按涨跌幅或添加日期排序时,获取更多结果以确保排序准确性 - effective_search_size = min(1000, request.search_size * 10) # 最多获取1000个 - logger.info(f"Using expanded search size {effective_search_size} for {request.sort_by} sorting") - - # 构建查询体 - search_body = {} - match_type = "keyword" - - # 根据搜索类型构建不同的查询 - if semantic_weight == 0: - # 纯关键词搜索 - search_body = { - "query": build_keyword_query(cleaned_query or request.query, stock_filters), - "size": effective_search_size # 使用有效搜索大小 - } - match_type = "keyword" - - elif semantic_weight == 1.0 and request.use_knn and embedding: - # 纯KNN语义搜索 - filter_query = None - if stock_filters: - filter_query = { - "nested": { - "path": "stocks", - "query": { - "bool": { - "should": [ - {"terms": {"stocks.stock_name": stock_filters}}, - {"terms": {"stocks.stock_code": stock_filters}} - ] - } - } - } - } - - search_body = { - "knn": { - "field": "description_embedding", - "query_vector": embedding, - "k": effective_search_size, # 使用有效搜索大小 - "num_candidates": max(effective_search_size + 50, min(effective_search_size * 2, 10000)) # 确保 num_candidates > k - }, - "size": effective_search_size - } - - if filter_query: - search_body["knn"]["filter"] = filter_query - - match_type = "semantic_knn" - - elif request.use_knn and embedding: - # 混合搜索(KNN + 关键词) - hybrid_body = build_hybrid_knn_query( - cleaned_query or request.query, - embedding, - semantic_weight, - stock_filters, - k=effective_search_size # 使用有效搜索大小 - ) - search_body = hybrid_body - search_body["size"] = effective_search_size - match_type = "hybrid_knn" - - else: - # 传统混合搜索(script_score方式,作为后备) - es_query = build_hybrid_query( - cleaned_query or request.query, - embedding, - semantic_weight, - stock_filters - ) - search_body = { - "query": es_query, - "size": effective_search_size # 使用有效搜索大小 - } - match_type = "hybrid" - - # 添加高亮和源过滤 - search_body.update({ - "highlight": { - "fields": { - "concept": {}, - "description": {"fragment_size": 150}, - "stocks_reason": {"fragment_size": 150}, - "stocks_full_text": {"fragment_size": 150} - } - }, - "_source": { - "excludes": ["description_embedding"] - }, - "track_total_hits": True - }) - - # 执行搜索(增加超时时间) - es_response = es_client.search( - index=INDEX_NAME, - body=search_body, - timeout="30s" - ) - - # 收集结果 - all_results = [] - concept_ids = [] - - for hit in es_response['hits']['hits']: - source = hit['_source'] - concept_ids.append(source['concept_id']) - - # 提取股票信息 - stocks = [] - for stock in source.get('stocks', [])[:20]: - stock_info = StockInfo( - stock_name=stock.get('stock_name', ''), - stock_code=stock.get('stock_code', ''), - reason=stock.get('reason'), - industry=stock.get('行业'), - project=stock.get('项目') - ) - stocks.append(stock_info) - - # 提取高亮信息 - highlights = {} - if 'highlight' in hit: - highlights = hit['highlight'] - - # 构建结果 - result = ConceptResult( - concept_id=source['concept_id'], - concept=source['concept'], - description=source.get('description'), - stocks=stocks, - stock_count=len(source.get('stocks', [])), - happened_times=source.get('happened_times'), - score=hit['_score'], - match_type=match_type, - highlights=highlights, - price_info=None - ) - all_results.append(result) - - # 【关键修改】:始终获取涨跌幅数据,无论排序方式 - # 这样用户可以看到涨跌幅信息,即使不按涨跌幅排序 - price_data = {} - actual_price_date = None - - if concept_ids: - # 获取所有结果的价格数据 - price_data = await get_concept_price_data(concept_ids, request.trade_date) - if price_data: - actual_price_date = next(iter(price_data.values())).trade_date - - # 填充涨跌幅信息 - for result in all_results: - result.price_info = price_data.get(result.concept_id) - - # 根据排序方式排序 - if request.sort_by == "change_pct": - # 按涨跌幅排序(降序) - all_results.sort( - key=lambda x: ( - x.price_info.avg_change_pct - if x.price_info and x.price_info.avg_change_pct is not None - else -999 - ), - reverse=True - ) - elif request.sort_by == "stock_count": - all_results.sort(key=lambda x: x.stock_count, reverse=True) - elif request.sort_by == "concept_name": - all_results.sort(key=lambda x: x.concept) - elif request.sort_by == "added_date": - # 按添加日期排序(降序 - 最新的在前) - all_results.sort( - key=lambda x: ( - x.happened_times[0] if x.happened_times and len(x.happened_times) > 0 else '1900-01-01' - ), - reverse=True - ) - # _score排序已经由ES处理 - - # 计算分页 - total_results = len(all_results) - total_pages = max(1, (total_results + request.size - 1) // request.size) - current_page = min(request.page, total_pages) - - # 获取当前页的结果 - start_idx = (current_page - 1) * request.size - end_idx = start_idx + request.size - page_results = all_results[start_idx:end_idx] - - # 计算耗时 - took_ms = int((datetime.now() - start_time).total_seconds() * 1000) - - # 构建响应 - response = SearchResponse( - total=es_response['hits']['total']['value'], - took_ms=took_ms, - results=page_results, - search_info={ - "query": request.query, - "cleaned_query": cleaned_query, - "semantic_weight": semantic_weight, - "keyword_weight": 1.0 - semantic_weight, - "match_type": match_type, - "stock_filters": stock_filters, - "has_embedding": bool(embedding), - "sort_by": request.sort_by, - "search_size": request.search_size, - "effective_search_size": effective_search_size, # 实际使用的搜索大小 - "use_knn": request.use_knn, - "actual_results": total_results - }, - price_date=actual_price_date, - page=current_page, - total_pages=total_pages - ) - - return response - - except Exception as e: - logger.error(f"Search error: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/concept/{concept_id}", tags=["Concepts"]) -async def get_concept( - concept_id: str, - trade_date: Optional[date] = Query(None, description="交易日期,格式:YYYY-MM-DD,默认返回最新日期数据") -): - """根据ID获取概念详情,包含涨跌幅数据""" - try: - result = es_client.get(index=INDEX_NAME, id=concept_id) - source = result['_source'] - - stocks_reason = {} - if 'stocks_reason' in source: - try: - stocks_reason = json.loads(source['stocks_reason']) - except: - pass - - price_data = await get_concept_price_data([concept_id], trade_date) - price_info = price_data.get(concept_id) - - return { - "concept_id": source['concept_id'], - "concept": source['concept'], - "description": source.get('description'), - "stocks": source.get('stocks', []), - "stocks_reason": stocks_reason, - "happened_times": source.get('happened_times'), - "created_at": source.get('created_at'), - "price_info": price_info - } - except Exception as e: - if "NotFoundError" in str(type(e)): - raise HTTPException(status_code=404, detail="概念不存在") - logger.error(f"Get concept error: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/stock/{stock_code}/concepts", response_model=StockConceptsResponse, tags=["Stocks"]) -async def get_stock_concepts( - stock_code: str, - size: int = Query(50, ge=1, le=200, description="返回概念数量"), - sort_by: str = Query("stock_count", description="排序方式: stock_count, concept_name, recent"), - include_description: bool = Query(True, description="是否包含概念描述"), - trade_date: Optional[date] = Query(None, description="交易日期,格式:YYYY-MM-DD,默认返回最新日期数据") -): - """根据股票代码查询相关概念,包含涨跌幅数据""" - try: - query = { - "nested": { - "path": "stocks", - "query": { - "bool": { - "should": [ - {"term": {"stocks.stock_code": stock_code}}, - {"term": {"stocks.stock_name": stock_code}} - ] - } - } - } - } - - sort_rules = [] - if sort_by == "stock_count": - sort_rules.append({ - "_script": { - "type": "number", - "script": { - "source": "params._source.stocks.size()" - }, - "order": "desc" - } - }) - elif sort_by == "concept_name": - sort_rules.append({"concept.keyword": {"order": "asc"}}) - elif sort_by == "recent": - sort_rules.append({"happened_times": {"order": "desc", "missing": "_last"}}) - - search_body = { - "query": query, - "size": size, - "sort": sort_rules if sort_rules else [{"_score": {"order": "desc"}}], - "_source": { - "includes": ["concept_id", "concept", "description", "stocks", "happened_times", "created_at"], - "excludes": ["description_embedding", "stocks_reason"] if not include_description else [ - "description_embedding"] - } - } - - es_response = es_client.search(index=INDEX_NAME, body=search_body) - - concept_ids = [] - for hit in es_response['hits']['hits']: - concept_ids.append(hit['_source']['concept_id']) - - price_data = await get_concept_price_data(concept_ids, trade_date) - - actual_price_date = None - if price_data: - actual_price_date = next(iter(price_data.values())).trade_date if price_data else None - - concepts = [] - stock_info = None - - for hit in es_response['hits']['hits']: - source = hit['_source'] - concept_id = source['concept_id'] - - stock_detail = None - for stock in source.get('stocks', []): - if stock.get('stock_code') == stock_code or stock.get('stock_name') == stock_code: - stock_detail = stock - if not stock_info: - stock_info = { - "stock_code": stock.get('stock_code'), - "stock_name": stock.get('stock_name') - } - break - - concept = StockConceptInfo( - concept_id=concept_id, - concept=source['concept'], - stock_count=len(source.get('stocks', [])), - happened_times=source.get('happened_times'), - stock_detail=stock_detail, - price_info=price_data.get(concept_id) - ) - - if include_description: - concept.description = source.get('description') - - concepts.append(concept) - - stats = { - "total_concepts": es_response['hits']['total']['value'], - "returned_concepts": len(concepts), - "stock_info": stock_info - } - - concept_categories = {} - for concept in concepts: - concept_name = concept.concept - if '新能源' in concept_name: - category = '新能源' - elif '半导体' in concept_name or '芯片' in concept_name: - category = '半导体' - elif '人工智能' in concept_name or 'AI' in concept_name: - category = '人工智能' - elif '医' in concept_name: - category = '医药' - elif '金融' in concept_name or '银行' in concept_name: - category = '金融' - else: - category = '其他' - - if category not in concept_categories: - concept_categories[category] = 0 - concept_categories[category] += 1 - - stats["concept_categories"] = concept_categories - - return StockConceptsResponse( - stock_code=stock_code, - stats=stats, - concepts=concepts, - price_date=actual_price_date - ) - - except Exception as e: - logger.error(f"Get stock concepts error: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/stock/search", response_model=StockSearchResponse, tags=["Stocks"]) -async def search_stocks( - keyword: str = Query(..., description="股票关键词"), - size: int = Query(20, ge=1, le=100, description="返回数量") -): - """搜索股票名称或代码""" - try: - query = { - "nested": { - "path": "stocks", - "query": { - "bool": { - "should": [ - {"wildcard": {"stocks.stock_code": f"*{keyword}*"}}, - {"match": {"stocks.stock_name": keyword}} - ] - } - }, - "inner_hits": { - "size": 10, - "_source": ["stock_code", "stock_name"] - } - } - } - - search_body = { - "query": query, - "size": 0, - "aggs": { - "unique_stocks": { - "nested": { - "path": "stocks" - }, - "aggs": { - "stock_codes": { - "terms": { - "field": "stocks.stock_code", - "size": size * 2 - }, - "aggs": { - "stock_names": { - "terms": { - "field": "stocks.stock_name", - "size": 1 - } - } - } - } - } - } - } - } - - es_response = es_client.search(index=INDEX_NAME, body=search_body) - - stocks = [] - buckets = es_response['aggregations']['unique_stocks']['stock_codes']['buckets'] - - for bucket in buckets: - stock_code = bucket['key'] - concept_count = bucket['doc_count'] - - name_buckets = bucket['stock_names']['buckets'] - stock_name = name_buckets[0]['key'] if name_buckets else stock_code - - if keyword in stock_code or keyword in stock_name: - stocks.append(StockSearchResult( - stock_code=stock_code, - stock_name=stock_name, - concept_count=concept_count - )) - - stocks.sort(key=lambda x: x.concept_count, reverse=True) - - return StockSearchResponse( - total=len(stocks), - stocks=stocks[:size] - ) - - except Exception as e: - logger.error(f"Search stocks error: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/price/latest", tags=["Price"]) -async def get_latest_price_date(): - """获取最新的涨跌幅数据日期""" - try: - latest_date = await get_latest_trade_date() - return { - "latest_trade_date": latest_date, - "has_data": latest_date is not None - } - except Exception as e: - logger.error(f"Get latest price date error: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/concept/{concept_id}/price-timeseries", response_model=PriceTimeSeriesResponse, tags=["Price"]) -async def get_concept_price_timeseries( - concept_id: str, - start_date: date = Query(..., description="开始日期,格式:YYYY-MM-DD"), - end_date: date = Query(..., description="结束日期,格式:YYYY-MM-DD") -): - """获取概念在指定日期范围内的涨跌幅时间序列数据""" - if not mysql_pool: - logger.warning(f"[PriceTimeseries] MySQL 连接不可用,返回空时间序列数据") - # 返回空时间序列而不是 503 错误 - return PriceTimeSeriesResponse( - concept_id=concept_id, - concept_name=concept_id, # 无法查询名称,使用 ID - start_date=start_date, - end_date=end_date, - data_points=0, - timeseries=[] - ) - - if start_date > end_date: - raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期") - - try: - async with mysql_pool.acquire() as conn: - async with conn.cursor(aiomysql.DictCursor) as cursor: - select_fields = """ - trade_date, - concept_name, - avg_change_pct, - stock_count - """ - - query = f""" - SELECT {select_fields} - FROM concept_daily_stats - WHERE concept_id = %s - AND trade_date >= %s - AND trade_date <= %s - ORDER BY trade_date ASC - """ - - await cursor.execute(query, (concept_id, start_date, end_date)) - rows = await cursor.fetchall() - - if not rows: - raise HTTPException( - status_code=404, - detail=f"未找到概念ID {concept_id} 在 {start_date} 至 {end_date} 期间的数据" - ) - - timeseries = [] - concept_name = "" - - for row in rows: - if not concept_name and row.get('concept_name'): - concept_name = row['concept_name'] - - item = PriceTimeSeriesItem( - trade_date=row['trade_date'], - avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] is not None else None, - stock_count=row.get('stock_count') - ) - - timeseries.append(item) - - return PriceTimeSeriesResponse( - concept_id=concept_id, - concept_name=concept_name or concept_id, - start_date=start_date, - end_date=end_date, - data_points=len(timeseries), - timeseries=timeseries - ) - - except HTTPException: - raise - except Exception as e: - logger.error(f"Error fetching concept price timeseries: {e}") - raise HTTPException(status_code=500, detail=f"获取时间序列数据失败: {str(e)}") - - -# 概念统计相关的数据模型 -class ConceptStatItem(BaseModel): - name: str - concept_id: Optional[str] = None - change_pct: Optional[float] = None - stock_count: Optional[int] = None - news_count: Optional[int] = None - report_count: Optional[int] = None - total_mentions: Optional[int] = None - volatility: Optional[float] = None - avg_change: Optional[float] = None - max_change: Optional[float] = None - consecutive_days: Optional[int] = None - total_change: Optional[float] = None - avg_daily: Optional[float] = None - - -class ConceptStatistics(BaseModel): - hot_concepts: List[ConceptStatItem] - cold_concepts: List[ConceptStatItem] - active_concepts: List[ConceptStatItem] - volatile_concepts: List[ConceptStatItem] - momentum_concepts: List[ConceptStatItem] - summary: Dict[str, Any] - - -class ConceptStatisticsResponse(BaseModel): - success: bool - data: ConceptStatistics - params: Dict[str, Any] - note: Optional[str] = None - - -@app.get("/statistics", response_model=ConceptStatisticsResponse, tags=["Statistics"]) -async def get_concept_statistics( - days: Optional[int] = Query(None, ge=1, le=90, description="统计天数范围(与start_date/end_date互斥)"), - start_date: Optional[date] = Query(None, description="开始日期,格式:YYYY-MM-DD"), - end_date: Optional[date] = Query(None, description="结束日期,格式:YYYY-MM-DD"), - min_stock_count: int = Query(3, ge=1, description="最少股票数量过滤") -): - """获取概念板块统计数据 - 涨幅榜、跌幅榜、活跃榜、波动榜、连涨榜""" - - from datetime import datetime, timedelta - - # 如果 MySQL 不可用,直接返回示例数据(而不是返回 503) - if not mysql_pool: - logger.warning("[Statistics] MySQL 连接不可用,使用示例数据") - - # 计算日期范围 - if days is not None and (start_date is not None or end_date is not None): - pass # 参数冲突,但仍使用 days - - if start_date is not None and end_date is not None: - pass # 使用提供的日期 - elif days is not None: - end_date = datetime.now().date() - start_date = end_date - timedelta(days=days) - elif start_date is not None: - end_date = datetime.now().date() - elif end_date is not None: - start_date = end_date - timedelta(days=7) - else: - end_date = datetime.now().date() - start_date = end_date - timedelta(days=7) - - # 返回示例数据(与 except 块中相同) - fallback_statistics = ConceptStatistics( - hot_concepts=[ - ConceptStatItem(name="小米大模型", change_pct=12.45, stock_count=24, news_count=18), - ConceptStatItem(name="人工智能", change_pct=8.76, stock_count=45, news_count=12), - ConceptStatItem(name="新能源汽车", change_pct=6.54, stock_count=38, news_count=8), - ConceptStatItem(name="芯片概念", change_pct=5.43, stock_count=52, news_count=15), - ConceptStatItem(name="生物医药", change_pct=4.21, stock_count=28, news_count=6), - ], - cold_concepts=[ - ConceptStatItem(name="房地产", change_pct=-5.76, stock_count=33, news_count=5), - ConceptStatItem(name="煤炭开采", change_pct=-4.32, stock_count=25, news_count=3), - ConceptStatItem(name="钢铁冶炼", change_pct=-3.21, stock_count=28, news_count=4), - ConceptStatItem(name="传统零售", change_pct=-2.98, stock_count=19, news_count=2), - ConceptStatItem(name="纺织服装", change_pct=-2.45, stock_count=15, news_count=2), - ], - active_concepts=[ - ConceptStatItem(name="人工智能", news_count=45, report_count=15, total_mentions=60), - ConceptStatItem(name="芯片概念", news_count=42, report_count=12, total_mentions=54), - ConceptStatItem(name="新能源汽车", news_count=38, report_count=8, total_mentions=46), - ConceptStatItem(name="生物医药", news_count=28, report_count=6, total_mentions=34), - ConceptStatItem(name="量子科技", news_count=25, report_count=5, total_mentions=30), - ], - volatile_concepts=[ - ConceptStatItem(name="区块链", volatility=25.6, avg_change=2.1, max_change=15.2), - ConceptStatItem(name="元宇宙", volatility=23.8, avg_change=1.8, max_change=13.9), - ConceptStatItem(name="虚拟现实", volatility=21.2, avg_change=-0.5, max_change=10.1), - ConceptStatItem(name="游戏概念", volatility=19.7, avg_change=3.2, max_change=12.8), - ConceptStatItem(name="在线教育", volatility=18.3, avg_change=-1.1, max_change=8.1), - ], - momentum_concepts=[ - ConceptStatItem(name="数字经济", consecutive_days=6, total_change=19.2, avg_daily=3.2), - ConceptStatItem(name="云计算", consecutive_days=5, total_change=16.8, avg_daily=3.36), - ConceptStatItem(name="物联网", consecutive_days=4, total_change=13.1, avg_daily=3.28), - ConceptStatItem(name="大数据", consecutive_days=4, total_change=12.4, avg_daily=3.1), - ConceptStatItem(name="工业互联网", consecutive_days=3, total_change=9.6, avg_daily=3.2), - ], - summary={ - 'total_concepts': 500, - 'positive_count': 320, - 'negative_count': 180, - 'avg_change': 1.8, - 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'date_range': f"{start_date} 至 {end_date}", - 'days': (end_date - start_date).days + 1, - 'start_date': str(start_date), - 'end_date': str(end_date) - } - ) - - return ConceptStatisticsResponse( - success=True, - data=fallback_statistics, - params={ - 'days': (end_date - start_date).days + 1, - 'min_stock_count': min_stock_count, - 'start_date': str(start_date), - 'end_date': str(end_date) - }, - note="MySQL 连接不可用,使用示例数据" - ) - - try: - import random - - # 参数验证和日期范围计算 - if days is not None and (start_date is not None or end_date is not None): - raise HTTPException(status_code=400, detail="days参数与start_date/end_date参数不能同时使用") - - if start_date is not None and end_date is not None: - if start_date > end_date: - raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期") - # 限制日期范围不超过90天 - if (end_date - start_date).days > 90: - raise HTTPException(status_code=400, detail="日期范围不能超过90天") - elif days is not None: - # 使用days参数 - end_date = datetime.now().date() - start_date = end_date - timedelta(days=days) - elif start_date is not None: - # 只有开始日期,默认到今天 - end_date = datetime.now().date() - if (end_date - start_date).days > 90: - raise HTTPException(status_code=400, detail="日期范围不能超过90天") - elif end_date is not None: - # 只有结束日期,默认前7天 - start_date = end_date - timedelta(days=7) - else: - # 默认最近7天 - end_date = datetime.now().date() - start_date = end_date - timedelta(days=7) - - async with mysql_pool.acquire() as conn: - async with conn.cursor(aiomysql.DictCursor) as cursor: - - # 1. 获取涨幅榜 - 基于平均涨跌幅排序 - hot_query = """ - SELECT - concept_id, - concept_name, - AVG(avg_change_pct) as avg_change_pct, - AVG(stock_count) as avg_stock_count, - COUNT(*) as trading_days - FROM concept_daily_stats - WHERE trade_date >= %s AND trade_date <= %s - AND avg_change_pct IS NOT NULL - AND stock_count >= %s - GROUP BY concept_id, concept_name - HAVING COUNT(*) >= 2 - ORDER BY AVG(avg_change_pct) DESC - LIMIT 5 - """ - await cursor.execute(hot_query, (start_date, end_date, min_stock_count)) - hot_rows = await cursor.fetchall() - - # 2. 获取跌幅榜 - 基于平均涨跌幅排序(负值) - cold_query = """ - SELECT - concept_id, - concept_name, - AVG(avg_change_pct) as avg_change_pct, - AVG(stock_count) as avg_stock_count, - COUNT(*) as trading_days - FROM concept_daily_stats - WHERE trade_date >= %s AND trade_date <= %s - AND avg_change_pct IS NOT NULL - AND stock_count >= %s - GROUP BY concept_id, concept_name - HAVING COUNT(*) >= 2 - ORDER BY AVG(avg_change_pct) ASC - LIMIT 5 - """ - await cursor.execute(cold_query, (start_date, end_date, min_stock_count)) - cold_rows = await cursor.fetchall() - - # 3. 获取活跃榜 - 基于交易天数和股票数量 - active_query = """ - SELECT - concept_id, - concept_name, - COUNT(*) as trading_days, - AVG(stock_count) as avg_stock_count, - MAX(stock_count) as max_stock_count - FROM concept_daily_stats - WHERE trade_date >= %s AND trade_date <= %s - AND stock_count >= %s - GROUP BY concept_id, concept_name - ORDER BY COUNT(*) DESC, AVG(stock_count) DESC - LIMIT 5 - """ - await cursor.execute(active_query, (start_date, end_date, min_stock_count)) - active_rows = await cursor.fetchall() - - # 4. 获取波动榜 - 基于涨跌幅标准差 - volatile_query = """ - SELECT - concept_id, - concept_name, - STDDEV(avg_change_pct) as volatility, - AVG(avg_change_pct) as avg_change_pct, - MAX(avg_change_pct) as max_change_pct, - AVG(stock_count) as avg_stock_count - FROM concept_daily_stats - WHERE trade_date >= %s AND trade_date <= %s - AND avg_change_pct IS NOT NULL - AND stock_count >= %s - GROUP BY concept_id, concept_name - HAVING COUNT(*) >= 3 AND STDDEV(avg_change_pct) IS NOT NULL - ORDER BY STDDEV(avg_change_pct) DESC - LIMIT 5 - """ - await cursor.execute(volatile_query, (start_date, end_date, min_stock_count)) - volatile_rows = await cursor.fetchall() - - # 5. 获取连涨榜 - 基于连续正涨幅天数(简化版本) - momentum_query = """ - SELECT - concept_id, - concept_name, - COUNT(*) as positive_days, - SUM(avg_change_pct) as total_change, - AVG(avg_change_pct) as avg_change_pct, - AVG(stock_count) as avg_stock_count - FROM concept_daily_stats - WHERE trade_date >= %s AND trade_date <= %s - AND avg_change_pct > 0 - AND stock_count >= %s - GROUP BY concept_id, concept_name - HAVING COUNT(*) >= 2 - ORDER BY COUNT(*) DESC, AVG(avg_change_pct) DESC - LIMIT 5 - """ - await cursor.execute(momentum_query, (start_date, end_date, min_stock_count)) - momentum_rows = await cursor.fetchall() - - # 6. 获取总体统计 - total_query = """ - SELECT - COUNT(DISTINCT concept_id) as total_concepts, - COUNT(DISTINCT CASE WHEN avg_change_pct > 0 THEN concept_id END) as positive_concepts, - COUNT(DISTINCT CASE WHEN avg_change_pct < 0 THEN concept_id END) as negative_concepts, - AVG(avg_change_pct) as overall_avg_change - FROM concept_daily_stats - WHERE trade_date >= %s AND trade_date <= %s - AND avg_change_pct IS NOT NULL - """ - await cursor.execute(total_query, (start_date, end_date)) - total_row = await cursor.fetchone() - - # 构建响应数据 - def build_concept_items(rows, item_type): - items = [] - for row in rows: - item = ConceptStatItem( - name=row['concept_name'], - concept_id=row.get('concept_id') - ) - - if item_type == 'hot' or item_type == 'cold': - item.change_pct = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0 - item.stock_count = int(row['avg_stock_count']) if row['avg_stock_count'] else 0 - item.news_count = int(row['trading_days']) if row['trading_days'] else 0 - - elif item_type == 'active': - item.news_count = int(row['trading_days']) if row['trading_days'] else 0 - item.stock_count = int(row['avg_stock_count']) if row['avg_stock_count'] else 0 - item.report_count = max(1, int(row['trading_days'] * 0.3)) if row['trading_days'] else 1 - item.total_mentions = item.news_count + item.report_count - - elif item_type == 'volatile': - item.volatility = round(float(row['volatility']), 2) if row['volatility'] else 0.0 - item.avg_change = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0 - item.max_change = round(float(row['max_change_pct']), 2) if row['max_change_pct'] else 0.0 - - elif item_type == 'momentum': - item.consecutive_days = int(row['positive_days']) if row['positive_days'] else 0 - item.total_change = round(float(row['total_change']), 2) if row['total_change'] else 0.0 - item.avg_daily = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0 - - items.append(item) - return items - - # 构建统计数据 - statistics = ConceptStatistics( - hot_concepts=build_concept_items(hot_rows, 'hot'), - cold_concepts=build_concept_items(cold_rows, 'cold'), - active_concepts=build_concept_items(active_rows, 'active'), - volatile_concepts=build_concept_items(volatile_rows, 'volatile'), - momentum_concepts=build_concept_items(momentum_rows, 'momentum'), - summary={ - 'total_concepts': int(total_row['total_concepts']) if total_row['total_concepts'] else 0, - 'positive_count': int(total_row['positive_concepts']) if total_row['positive_concepts'] else 0, - 'negative_count': int(total_row['negative_concepts']) if total_row['negative_concepts'] else 0, - 'avg_change': round(float(total_row['overall_avg_change']), 2) if total_row['overall_avg_change'] else 0.0, - 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'date_range': f"{start_date} 至 {end_date}", - 'days': max(1, (end_date - start_date).days + 1), # 包含起始日期 - 'start_date': str(start_date), - 'end_date': str(end_date) - } - ) - - # 如果某些榜单数据不足,使用示例数据补充 - if not statistics.hot_concepts: - statistics.hot_concepts = [ - ConceptStatItem(name="人工智能", change_pct=8.76, stock_count=45, news_count=12), - ConceptStatItem(name="新能源汽车", change_pct=6.54, stock_count=38, news_count=8), - ConceptStatItem(name="芯片概念", change_pct=5.43, stock_count=52, news_count=15), - ConceptStatItem(name="生物医药", change_pct=4.21, stock_count=28, news_count=6), - ConceptStatItem(name="5G通信", change_pct=3.98, stock_count=35, news_count=9), - ] - - if not statistics.cold_concepts: - statistics.cold_concepts = [ - ConceptStatItem(name="房地产", change_pct=-5.76, stock_count=33, news_count=5), - ConceptStatItem(name="煤炭开采", change_pct=-4.32, stock_count=25, news_count=3), - ConceptStatItem(name="钢铁冶炼", change_pct=-3.21, stock_count=28, news_count=4), - ConceptStatItem(name="传统零售", change_pct=-2.98, stock_count=19, news_count=2), - ConceptStatItem(name="纺织服装", change_pct=-2.45, stock_count=15, news_count=2), - ] - - return ConceptStatisticsResponse( - success=True, - data=statistics, - params={ - 'days': max(1, (end_date - start_date).days + 1), - 'min_stock_count': min_stock_count, - 'start_date': str(start_date), - 'end_date': str(end_date) - } - ) - - except Exception as e: - logger.error(f"Error fetching concept statistics: {e}") - - # 返回示例数据作为fallback - fallback_statistics = ConceptStatistics( - hot_concepts=[ - ConceptStatItem(name="小米大模型", change_pct=12.45, stock_count=24, news_count=18), - ConceptStatItem(name="人工智能", change_pct=8.76, stock_count=45, news_count=12), - ConceptStatItem(name="新能源汽车", change_pct=6.54, stock_count=38, news_count=8), - ConceptStatItem(name="芯片概念", change_pct=5.43, stock_count=52, news_count=15), - ConceptStatItem(name="生物医药", change_pct=4.21, stock_count=28, news_count=6), - ], - cold_concepts=[ - ConceptStatItem(name="房地产", change_pct=-5.76, stock_count=33, news_count=5), - ConceptStatItem(name="煤炭开采", change_pct=-4.32, stock_count=25, news_count=3), - ConceptStatItem(name="钢铁冶炼", change_pct=-3.21, stock_count=28, news_count=4), - ConceptStatItem(name="传统零售", change_pct=-2.98, stock_count=19, news_count=2), - ConceptStatItem(name="纺织服装", change_pct=-2.45, stock_count=15, news_count=2), - ], - active_concepts=[ - ConceptStatItem(name="人工智能", news_count=45, report_count=15, total_mentions=60), - ConceptStatItem(name="芯片概念", news_count=42, report_count=12, total_mentions=54), - ConceptStatItem(name="新能源汽车", news_count=38, report_count=8, total_mentions=46), - ConceptStatItem(name="生物医药", news_count=28, report_count=6, total_mentions=34), - ConceptStatItem(name="量子科技", news_count=25, report_count=5, total_mentions=30), - ], - volatile_concepts=[ - ConceptStatItem(name="区块链", volatility=25.6, avg_change=2.1, max_change=15.2), - ConceptStatItem(name="元宇宙", volatility=23.8, avg_change=1.8, max_change=13.9), - ConceptStatItem(name="虚拟现实", volatility=21.2, avg_change=-0.5, max_change=10.1), - ConceptStatItem(name="游戏概念", volatility=19.7, avg_change=3.2, max_change=12.8), - ConceptStatItem(name="在线教育", volatility=18.3, avg_change=-1.1, max_change=8.1), - ], - momentum_concepts=[ - ConceptStatItem(name="数字经济", consecutive_days=6, total_change=19.2, avg_daily=3.2), - ConceptStatItem(name="云计算", consecutive_days=5, total_change=16.8, avg_daily=3.36), - ConceptStatItem(name="物联网", consecutive_days=4, total_change=13.1, avg_daily=3.28), - ConceptStatItem(name="大数据", consecutive_days=4, total_change=12.4, avg_daily=3.1), - ConceptStatItem(name="工业互联网", consecutive_days=3, total_change=9.6, avg_daily=3.2), - ], - summary={ - 'total_concepts': 500, - 'positive_count': 320, - 'negative_count': 180, - 'avg_change': 1.8, - 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'date_range': f"{start_date} 至 {end_date}", - 'days': max(1, (end_date - start_date).days + 1), - 'start_date': str(start_date), - 'end_date': str(end_date) - } - ) - - return ConceptStatisticsResponse( - success=True, - data=fallback_statistics, - params={ - 'days': max(1, (end_date - start_date).days + 1), - 'min_stock_count': min_stock_count, - 'start_date': str(start_date), - 'end_date': str(end_date) - }, - note=f"使用示例数据,原因: {str(e)}" - ) - - -# 主函数 -if __name__ == "__main__": - import uvicorn - - uvicorn.run( - "concept_api:app", - host="0.0.0.0", - port=6801, - reload=True, - log_level="info" - ) \ No newline at end of file diff --git a/concept_api_openapi.json b/concept_api_openapi.json deleted file mode 100644 index 7fa96034..00000000 --- a/concept_api_openapi.json +++ /dev/null @@ -1,1096 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "概念搜索API", - "description": "支持语义和关键词混合搜索的概念库API,包含概念涨跌幅数据。\n\n## 功能特性\n- 支持 KNN 优化的语义搜索\n- 关键词 + 语义混合搜索\n- 概念涨跌幅数据查询\n- 股票关联概念查询\n- 概念统计排行榜\n\n## 技术栈\n- FastAPI + Elasticsearch\n- OpenAI Embedding (qwen3-embedding-8b)\n- MySQL (概念涨跌幅数据)", - "version": "1.2.0", - "contact": { - "name": "ValueFrontier", - "url": "https://valuefrontier.cn/concept-api" - } - }, - "servers": [ - { - "url": "http://localhost:6801", - "description": "本地开发服务器" - }, - { - "url": "https://api.valuefrontier.cn:6801", - "description": "生产服务器" - } - ], - "tags": [ - { - "name": "Health", - "description": "健康检查接口" - }, - { - "name": "Search", - "description": "概念搜索接口" - }, - { - "name": "Concepts", - "description": "概念详情接口" - }, - { - "name": "Stocks", - "description": "股票相关接口" - }, - { - "name": "Price", - "description": "价格数据接口" - }, - { - "name": "Statistics", - "description": "统计数据接口" - } - ], - "paths": { - "/": { - "get": { - "tags": ["Health"], - "summary": "健康检查", - "description": "检查服务是否正常运行", - "operationId": "healthCheck", - "responses": { - "200": { - "description": "服务正常", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "healthy" - }, - "service": { - "type": "string", - "example": "概念搜索API" - }, - "version": { - "type": "string", - "example": "1.2.0" - } - } - } - } - } - } - } - } - }, - "/search": { - "post": { - "tags": ["Search"], - "summary": "搜索概念库", - "description": "支持 KNN 优化的语义搜索,混合搜索(KNN + 关键词),支持按涨跌幅、相关性等排序", - "operationId": "searchConcepts", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchRequest" - }, - "examples": { - "semantic_search": { - "summary": "语义搜索示例", - "value": { - "query": "新能源汽车电池技术", - "size": 10, - "page": 1, - "sort_by": "change_pct", - "use_knn": true - } - }, - "keyword_search": { - "summary": "关键词搜索示例", - "value": { - "query": "人工智能", - "size": 20, - "semantic_weight": 0, - "sort_by": "_score" - } - }, - "stock_filter": { - "summary": "股票过滤示例", - "value": { - "query": "芯片", - "filter_stocks": ["600519", "贵州茅台"], - "size": 10 - } - } - } - } - } - }, - "responses": { - "200": { - "description": "搜索成功", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchResponse" - } - } - } - }, - "500": { - "description": "服务器错误", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPError" - } - } - } - } - } - } - }, - "/concept/{concept_id}": { - "get": { - "tags": ["Concepts"], - "summary": "获取概念详情", - "description": "根据概念ID获取完整的概念信息,包含关联股票和涨跌幅数据", - "operationId": "getConcept", - "parameters": [ - { - "name": "concept_id", - "in": "path", - "required": true, - "description": "概念ID", - "schema": { - "type": "string" - }, - "example": "concept_ai_001" - }, - { - "name": "trade_date", - "in": "query", - "required": false, - "description": "交易日期,格式:YYYY-MM-DD,默认返回最新日期数据", - "schema": { - "type": "string", - "format": "date" - }, - "example": "2025-11-25" - } - ], - "responses": { - "200": { - "description": "获取成功", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConceptDetail" - } - } - } - }, - "404": { - "description": "概念不存在", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPError" - } - } - } - } - } - } - }, - "/concept/{concept_id}/price-timeseries": { - "get": { - "tags": ["Price"], - "summary": "获取概念价格时间序列", - "description": "获取指定概念在日期范围内的涨跌幅时间序列数据", - "operationId": "getConceptPriceTimeseries", - "parameters": [ - { - "name": "concept_id", - "in": "path", - "required": true, - "description": "概念ID", - "schema": { - "type": "string" - } - }, - { - "name": "start_date", - "in": "query", - "required": true, - "description": "开始日期,格式:YYYY-MM-DD", - "schema": { - "type": "string", - "format": "date" - }, - "example": "2025-11-01" - }, - { - "name": "end_date", - "in": "query", - "required": true, - "description": "结束日期,格式:YYYY-MM-DD", - "schema": { - "type": "string", - "format": "date" - }, - "example": "2025-11-25" - } - ], - "responses": { - "200": { - "description": "获取成功", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PriceTimeSeriesResponse" - } - } - } - }, - "400": { - "description": "参数错误(开始日期晚于结束日期)" - }, - "404": { - "description": "未找到数据" - } - } - } - }, - "/stock/{stock_code}/concepts": { - "get": { - "tags": ["Stocks"], - "summary": "获取股票关联概念", - "description": "根据股票代码或名称查询该股票关联的所有概念", - "operationId": "getStockConcepts", - "parameters": [ - { - "name": "stock_code", - "in": "path", - "required": true, - "description": "股票代码或名称", - "schema": { - "type": "string" - }, - "example": "600519" - }, - { - "name": "size", - "in": "query", - "required": false, - "description": "返回概念数量", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 50 - } - }, - { - "name": "sort_by", - "in": "query", - "required": false, - "description": "排序方式", - "schema": { - "type": "string", - "enum": ["stock_count", "concept_name", "recent"], - "default": "stock_count" - } - }, - { - "name": "include_description", - "in": "query", - "required": false, - "description": "是否包含概念描述", - "schema": { - "type": "boolean", - "default": true - } - }, - { - "name": "trade_date", - "in": "query", - "required": false, - "description": "交易日期,格式:YYYY-MM-DD", - "schema": { - "type": "string", - "format": "date" - } - } - ], - "responses": { - "200": { - "description": "获取成功", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StockConceptsResponse" - } - } - } - } - } - } - }, - "/stock/search": { - "get": { - "tags": ["Stocks"], - "summary": "搜索股票", - "description": "根据关键词搜索股票名称或代码", - "operationId": "searchStocks", - "parameters": [ - { - "name": "keyword", - "in": "query", - "required": true, - "description": "股票关键词", - "schema": { - "type": "string" - }, - "example": "茅台" - }, - { - "name": "size", - "in": "query", - "required": false, - "description": "返回数量", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - } - } - ], - "responses": { - "200": { - "description": "搜索成功", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StockSearchResponse" - } - } - } - } - } - } - }, - "/price/latest": { - "get": { - "tags": ["Price"], - "summary": "获取最新价格日期", - "description": "获取数据库中最新的涨跌幅数据日期", - "operationId": "getLatestPriceDate", - "responses": { - "200": { - "description": "获取成功", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "latest_trade_date": { - "type": "string", - "format": "date", - "nullable": true, - "example": "2025-11-25" - }, - "has_data": { - "type": "boolean", - "example": true - } - } - } - } - } - } - } - } - }, - "/statistics": { - "get": { - "tags": ["Statistics"], - "summary": "获取概念统计数据", - "description": "获取概念板块统计数据,包括涨幅榜、跌幅榜、活跃榜、波动榜、连涨榜", - "operationId": "getConceptStatistics", - "parameters": [ - { - "name": "days", - "in": "query", - "required": false, - "description": "统计天数范围(与 start_date/end_date 互斥)", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 90 - }, - "example": 7 - }, - { - "name": "start_date", - "in": "query", - "required": false, - "description": "开始日期,格式:YYYY-MM-DD", - "schema": { - "type": "string", - "format": "date" - } - }, - { - "name": "end_date", - "in": "query", - "required": false, - "description": "结束日期,格式:YYYY-MM-DD", - "schema": { - "type": "string", - "format": "date" - } - }, - { - "name": "min_stock_count", - "in": "query", - "required": false, - "description": "最少股票数量过滤", - "schema": { - "type": "integer", - "minimum": 1, - "default": 3 - } - } - ], - "responses": { - "200": { - "description": "获取成功", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConceptStatisticsResponse" - } - } - } - }, - "400": { - "description": "参数错误" - } - } - } - } - }, - "components": { - "schemas": { - "SearchRequest": { - "type": "object", - "required": ["query"], - "properties": { - "query": { - "type": "string", - "description": "搜索查询文本", - "example": "新能源汽车" - }, - "size": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 10, - "description": "每页返回结果数量" - }, - "page": { - "type": "integer", - "minimum": 1, - "default": 1, - "description": "页码" - }, - "search_size": { - "type": "integer", - "minimum": 10, - "maximum": 1000, - "default": 100, - "description": "搜索数量(从ES获取的结果数),用于排序后分页" - }, - "semantic_weight": { - "type": "number", - "minimum": 0, - "maximum": 1, - "nullable": true, - "description": "语义搜索权重(0-1),null表示自动计算" - }, - "filter_stocks": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "description": "过滤特定股票代码或名称" - }, - "trade_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "交易日期,格式:YYYY-MM-DD" - }, - "sort_by": { - "type": "string", - "enum": ["change_pct", "_score", "stock_count", "concept_name", "added_date"], - "default": "change_pct", - "description": "排序方式" - }, - "use_knn": { - "type": "boolean", - "default": true, - "description": "是否使用KNN搜索优化语义搜索" - } - } - }, - "SearchResponse": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "description": "总结果数" - }, - "took_ms": { - "type": "integer", - "description": "查询耗时(毫秒)" - }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConceptResult" - } - }, - "search_info": { - "type": "object", - "description": "搜索信息", - "properties": { - "query": { - "type": "string" - }, - "cleaned_query": { - "type": "string" - }, - "semantic_weight": { - "type": "number" - }, - "keyword_weight": { - "type": "number" - }, - "match_type": { - "type": "string", - "enum": ["keyword", "semantic", "hybrid", "semantic_knn", "hybrid_knn"] - }, - "stock_filters": { - "type": "array", - "items": { - "type": "string" - } - }, - "has_embedding": { - "type": "boolean" - }, - "sort_by": { - "type": "string" - }, - "use_knn": { - "type": "boolean" - } - } - }, - "price_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "价格数据日期" - }, - "page": { - "type": "integer", - "description": "当前页码" - }, - "total_pages": { - "type": "integer", - "description": "总页数" - } - } - }, - "ConceptResult": { - "type": "object", - "properties": { - "concept_id": { - "type": "string", - "description": "概念ID" - }, - "concept": { - "type": "string", - "description": "概念名称" - }, - "description": { - "type": "string", - "nullable": true, - "description": "概念描述" - }, - "stocks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StockInfo" - }, - "description": "关联股票列表(最多20条)" - }, - "stock_count": { - "type": "integer", - "description": "关联股票总数" - }, - "happened_times": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "description": "发生时间列表" - }, - "score": { - "type": "number", - "description": "搜索相关性分数" - }, - "match_type": { - "type": "string", - "enum": ["semantic", "keyword", "hybrid", "semantic_knn", "hybrid_knn"], - "description": "匹配类型" - }, - "highlights": { - "type": "object", - "nullable": true, - "description": "高亮匹配信息" - }, - "price_info": { - "$ref": "#/components/schemas/ConceptPriceInfo" - } - } - }, - "StockInfo": { - "type": "object", - "properties": { - "stock_name": { - "type": "string", - "description": "股票名称" - }, - "stock_code": { - "type": "string", - "description": "股票代码" - }, - "reason": { - "type": "string", - "nullable": true, - "description": "关联原因" - }, - "industry": { - "type": "string", - "nullable": true, - "description": "所属行业" - }, - "project": { - "type": "string", - "nullable": true, - "description": "相关项目" - } - } - }, - "ConceptPriceInfo": { - "type": "object", - "nullable": true, - "properties": { - "trade_date": { - "type": "string", - "format": "date", - "description": "交易日期" - }, - "avg_change_pct": { - "type": "number", - "nullable": true, - "description": "平均涨跌幅(%)" - } - } - }, - "ConceptDetail": { - "type": "object", - "properties": { - "concept_id": { - "type": "string" - }, - "concept": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - }, - "stocks": { - "type": "array", - "items": { - "type": "object" - } - }, - "stocks_reason": { - "type": "object", - "description": "股票关联原因详情" - }, - "happened_times": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "created_at": { - "type": "string", - "nullable": true - }, - "price_info": { - "$ref": "#/components/schemas/ConceptPriceInfo" - } - } - }, - "StockConceptsResponse": { - "type": "object", - "properties": { - "stock_code": { - "type": "string", - "description": "股票代码" - }, - "stats": { - "type": "object", - "properties": { - "total_concepts": { - "type": "integer", - "description": "关联概念总数" - }, - "returned_concepts": { - "type": "integer", - "description": "返回概念数" - }, - "stock_info": { - "type": "object", - "nullable": true, - "properties": { - "stock_code": { - "type": "string" - }, - "stock_name": { - "type": "string" - } - } - }, - "concept_categories": { - "type": "object", - "description": "概念分类统计" - } - } - }, - "concepts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StockConceptInfo" - } - }, - "price_date": { - "type": "string", - "format": "date", - "nullable": true - } - } - }, - "StockConceptInfo": { - "type": "object", - "properties": { - "concept_id": { - "type": "string" - }, - "concept": { - "type": "string" - }, - "stock_count": { - "type": "integer" - }, - "happened_times": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "stock_detail": { - "type": "object", - "nullable": true, - "description": "该股票在此概念中的详细信息" - }, - "price_info": { - "$ref": "#/components/schemas/ConceptPriceInfo" - } - } - }, - "StockSearchResponse": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "description": "搜索结果总数" - }, - "stocks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StockSearchResult" - } - } - } - }, - "StockSearchResult": { - "type": "object", - "properties": { - "stock_code": { - "type": "string" - }, - "stock_name": { - "type": "string" - }, - "concept_count": { - "type": "integer", - "description": "关联概念数量" - } - } - }, - "PriceTimeSeriesResponse": { - "type": "object", - "properties": { - "concept_id": { - "type": "string" - }, - "concept_name": { - "type": "string" - }, - "start_date": { - "type": "string", - "format": "date" - }, - "end_date": { - "type": "string", - "format": "date" - }, - "data_points": { - "type": "integer", - "description": "数据点数量" - }, - "timeseries": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PriceTimeSeriesItem" - } - } - } - }, - "PriceTimeSeriesItem": { - "type": "object", - "properties": { - "trade_date": { - "type": "string", - "format": "date" - }, - "avg_change_pct": { - "type": "number", - "nullable": true, - "description": "平均涨跌幅(%)" - }, - "stock_count": { - "type": "integer", - "nullable": true, - "description": "当日股票数量" - } - } - }, - "ConceptStatisticsResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "data": { - "$ref": "#/components/schemas/ConceptStatistics" - }, - "params": { - "type": "object", - "properties": { - "days": { - "type": "integer" - }, - "min_stock_count": { - "type": "integer" - }, - "start_date": { - "type": "string" - }, - "end_date": { - "type": "string" - } - } - }, - "note": { - "type": "string", - "nullable": true - } - } - }, - "ConceptStatistics": { - "type": "object", - "properties": { - "hot_concepts": { - "type": "array", - "description": "涨幅榜", - "items": { - "$ref": "#/components/schemas/ConceptStatItem" - } - }, - "cold_concepts": { - "type": "array", - "description": "跌幅榜", - "items": { - "$ref": "#/components/schemas/ConceptStatItem" - } - }, - "active_concepts": { - "type": "array", - "description": "活跃榜", - "items": { - "$ref": "#/components/schemas/ConceptStatItem" - } - }, - "volatile_concepts": { - "type": "array", - "description": "波动榜", - "items": { - "$ref": "#/components/schemas/ConceptStatItem" - } - }, - "momentum_concepts": { - "type": "array", - "description": "连涨榜", - "items": { - "$ref": "#/components/schemas/ConceptStatItem" - } - }, - "summary": { - "type": "object", - "description": "统计摘要", - "properties": { - "total_concepts": { - "type": "integer" - }, - "positive_count": { - "type": "integer" - }, - "negative_count": { - "type": "integer" - }, - "avg_change": { - "type": "number" - }, - "update_time": { - "type": "string" - }, - "date_range": { - "type": "string" - }, - "days": { - "type": "integer" - }, - "start_date": { - "type": "string" - }, - "end_date": { - "type": "string" - } - } - } - } - }, - "ConceptStatItem": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "概念名称" - }, - "concept_id": { - "type": "string", - "nullable": true - }, - "change_pct": { - "type": "number", - "nullable": true, - "description": "涨跌幅" - }, - "stock_count": { - "type": "integer", - "nullable": true, - "description": "股票数量" - }, - "news_count": { - "type": "integer", - "nullable": true, - "description": "新闻数量/交易天数" - }, - "report_count": { - "type": "integer", - "nullable": true - }, - "total_mentions": { - "type": "integer", - "nullable": true - }, - "volatility": { - "type": "number", - "nullable": true, - "description": "波动率" - }, - "avg_change": { - "type": "number", - "nullable": true - }, - "max_change": { - "type": "number", - "nullable": true - }, - "consecutive_days": { - "type": "integer", - "nullable": true, - "description": "连涨天数" - }, - "total_change": { - "type": "number", - "nullable": true, - "description": "累计涨幅" - }, - "avg_daily": { - "type": "number", - "nullable": true, - "description": "日均涨幅" - } - } - }, - "HTTPError": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "description": "错误详情" - } - } - } - } - } -} diff --git a/concept_hierarchy.json b/concept_hierarchy.json deleted file mode 100644 index 41951054..00000000 --- a/concept_hierarchy.json +++ /dev/null @@ -1,1176 +0,0 @@ -{ - "hierarchy": [ - { - "lv1": "人工智能", - "lv1_id": "L1-01", - "children": [ - { - "lv2": "AI算力基础设施", - "lv2_id": "L2-01-01", - "children": [ - { - "lv3": "AI芯片与半导体", - "lv3_id": "L3-01-01-01", - "concepts": [ - "AI算力芯片", - "AI芯片", - "ASIC", - "GPU概念股", - "HBM", - "国产GPU", - "国产算力芯片", - "英伟达H20", - "英伟达概念", - "算力", - "阿里AI芯片", - "中昊芯英概念股", - "TPU芯片" - ] - }, - { - "lv3": "AI服务器与组件", - "lv3_id": "L3-01-01-02", - "concepts": [ - "AI PCB", - "AI PCB英伟达M9", - "AI服务器钽电容", - "PCB", - "服务器零部件", - "服务器", - "电磁屏蔽", - "钽电容", - "超聚变" - ] - }, - { - "lv3": "数据中心(供电与散热)", - "lv3_id": "L3-01-01-03", - "concepts": [ - "AIDC供配电设备弹性", - "固态变压器SST", - "数据中心电力设备", - "数据中心", - "数据中心液冷", - "柴油发电机", - "微通道水冷板", - "液态金属散热", - "液冷数据中心", - "液冷", - "电池备份单元(BBU)", - "秦淮数据", - "磁悬浮压缩机", - "英伟达电源方案", - "铅酸电池", - "超级电容器", - "钻石散热", - "北美缺电AI电力" - ] - }, - { - "lv3": "高速互联(光/铜)", - "lv3_id": "L3-01-01-04", - "concepts": [ - "OCS光电路交换机", - "光通信CPO", - "光芯片", - "光纤", - "光通信", - "光纤列阵单元FAU", - "博通交换机", - "硅光技术", - "空芯光纤", - "薄膜铌酸锂", - "铜连接", - "铜互连" - ] - }, - { - "lv3": "区域与综合算力", - "lv3_id": "L3-01-01-05", - "concepts": [ - "上海算力", - "中国星际之门芜湖", - "四川算力", - "大厂算力订单", - "星际之门概念", - "核心城市智算算力", - "毫秒用算", - "杭州算力大会", - "甲骨文概念股", - "字节算力", - "云计算各厂商云" - ] - } - ] - }, - { - "lv2": "AI模型与应用", - "lv2_id": "L2-01-02", - "children": [ - { - "lv3": "大模型与平台", - "lv3_id": "L3-01-02-01", - "concepts": [ - "DeepSeek FP8", - "DeepSeek、国产算力", - "DeepSeek", - "国产大模型", - "昇腾异构计算架构-CANN", - "KIMI", - "Minimax", - "MOE模型", - "腾讯大模型", - "腾讯混元大模型", - "谷歌", - "豆包大模型", - "谷歌概念", - "通义千问阿里云", - "阿里云通义千问", - "阿里云", - "马斯克Grok3大模型", - "阶跃星辰" - ] - }, - { - "lv3": "AI原生应用与智能体", - "lv3_id": "L3-01-02-02", - "concepts": [ - "AI-细分延伸更新", - "AI Agent", - "AI应用智能体", - "AI智能体AI应用", - "AI编程", - "AI营销", - "AI语音助手", - "Manus", - "MCP", - "低代码", - "开发智能体", - "微软Azure云平台", - "腾讯元宝", - "秘塔AI", - "英伟达代理", - "阿里“千问”项目", - "阿里AI来听", - "阿里AI千问、灵光", - "AI伴侣", - "AI应用陪伴智能体", - "AI成人陪伴", - "AI陪伴", - "字节AI陪伴", - "AI教育", - "AI监考" - ] - }, - { - "lv3": "AI赋能行业", - "lv3_id": "L3-01-02-03", - "concepts": [ - "AI合集", - "AI应用AI语料", - "AI医药华为" - ] - } - ] - }, - { - "lv2": "AI生态与解决方案", - "lv2_id": "L2-01-03", - "children": [ - { - "lv3": "华为AI生态", - "lv3_id": "L3-01-03-01", - "concepts": [ - "华为910C", - "华为AI容器", - "华为通信大模型", - "华为昇腾", - "华为云", - "华为昇腾超节点", - "华为AI存储" - ] - }, - { - "lv3": "国产化与信创AI", - "lv3_id": "L3-01-03-02", - "concepts": [ - "AI一体机", - "DeepSeek智算一体机", - "昇腾推理一体机", - "一体机核心标的弹性测算", - "央国企AI一张图" - ] - }, - { - "lv3": "其他AI生态", - "lv3_id": "L3-01-03-03", - "concepts": [ - "字节豆包概念股", - "腾讯云及大模型合作公司" - ] - } - ] - } - ] - }, - { - "lv1": "机器人与智能制造", - "lv1_id": "L1-02", - "children": [ - { - "lv2": "人形机器人", - "lv2_id": "L2-02-01", - "children": [ - { - "lv3": "核心厂商与整机", - "lv3_id": "L3-02-01-01", - "concepts": [ - "Optimus特斯拉机器人", - "乐聚机器人", - "人形机器人Figure", - "人形机器人", - "优必选", - "人形机器人核心标的概览", - "优必选机器人", - "人形机器人核心标的估值弹性测算", - "华为人形机器人", - "各厂商机器人", - "天太机器人", - "天工机器人", - "宇树人形机器人", - "宇树机器人", - "小米智元机器人产业链机构版", - "小米机器人", - "小鹏机器人", - "开普勒机器人", - "智元机器人", - "松延动力机器人", - "特斯拉人形机器人弹性测算", - "特斯拉人形机器人", - "特斯拉人形机器人价值量", - "苹果机器人", - "美的库卡机器人", - "赛力斯机器人", - "荣耀华为人形机器人" - ] - }, - { - "lv3": "核心零部件", - "lv3_id": "L3-02-01-02", - "concepts": [ - "PCB轴向磁通电机", - "人形机器人万向节", - "人形机器人-滚柱丝杆丝杠", - "摆线减速器", - "轴向磁通电机" - ] - }, - { - "lv3": "材料与工艺", - "lv3_id": "L3-02-01-03", - "concepts": [ - "MIM概念", - "人形机器人腱绳", - "人形机器人轻量化-PEEK材料", - "冷锻产业链", - "机器人轻量化-PEEK", - "机器人轻量化-镁铝合金", - "机器人轻量化—碳纤维", - "金属粉末注射成形MIM", - "超高分子量聚乙烯纤维" - ] - }, - { - "lv3": "感知与交互", - "lv3_id": "L3-02-01-04", - "concepts": [ - "机器人动作捕捉", - "机器人皮肤仿生皮肤", - "机器人电子鼻", - "电子皮肤" - ] - }, - { - "lv3": "产业链与配套", - "lv3_id": "L3-02-01-05", - "concepts": [ - "人形机器产业链", - "奇瑞机器人潜在产业链", - "机器人零部件加工设备", - "机器人充电" - ] - } - ] - }, - { - "lv2": "工业及特种机器人", - "lv2_id": "L2-02-02", - "concepts": [ - "云深处", - "工业机器人", - "外骨骼机器人", - "机电", - "机器狗四足机器人", - "AGV" - ] - }, - { - "lv2": "智能制造与设备", - "lv2_id": "L2-02-03", - "concepts": [ - "超跌-机械板块", - "工业母机" - ] - } - ] - }, - { - "lv1": "半导体产业链", - "lv1_id": "L1-03", - "children": [ - { - "lv2": "半导体设备", - "lv2_id": "L2-03-01", - "concepts": [ - "PCB设备及耗材", - "上海微电子", - "光刻机宇量昇", - "光刻机", - "半导体设备", - "大湾区芯片展览会-新凯莱", - "新凯来示波器", - "新凯来概念股", - "电子束光刻机“羲之”" - ] - }, - { - "lv2": "半导体材料", - "lv2_id": "L2-03-02", - "concepts": [ - "PSPI", - "先进陶瓷", - "光刻胶", - "半导体材料", - "半导体抛光液", - "国产光刻胶", - "电子级玻璃纤维布", - "电子级玻璃纤维纱布", - "电子特气", - "电子特气六氟化钨", - "石英砂", - "磷化铟" - ] - }, - { - "lv2": "芯片设计与EDA", - "lv2_id": "L2-03-03", - "concepts": [ - "EDA", - "RISC-V", - "mobileye替代概念", - "功率半导体", - "国产芯片参股公司", - "摩尔线程IPO", - "摩尔线程", - "模拟芯片", - "沐曦集成", - "模拟厂商弹性测算", - "汽车芯片", - "第三代半导体", - "英诺赛科概念股", - "紫光展锐IPO", - "半导体设计" - ] - }, - { - "lv2": "先进封装与封测", - "lv2_id": "L2-03-04", - "concepts": [ - "华为910C", - "改良型半加成工艺mSAP", - "半导体封测", - "盛合晶微", - "玻璃基板", - "盛合晶微概念股" - ] - }, - { - "lv2": "存储芯片", - "lv2_id": "L2-03-05", - "concepts": [ - "SRAM存储", - "利基型存储DDR4", - "华为存储OceanStor", - "存储", - "存储芯片产业", - "存储芯片", - "忆阻器", - "磁电存储", - "长鑫存储", - "长鑫、长江产业链" - ] - }, - { - "lv2": "半导体综合与分销", - "lv2_id": "L2-03-06", - "concepts": [ - "半导体产业链", - "华为麒麟芯片", - "国产半导体", - "芯片分销概念", - "芯片替代", - "科特估半导体", - "英特尔概念股" - ] - } - ] - }, - { - "lv1": "消费电子", - "lv1_id": "L1-04", - "children": [ - { - "lv2": "AI终端与智能硬件", - "lv2_id": "L2-04-01", - "children": [ - { - "lv3": "AI PC与手机", - "lv3_id": "L3-04-01-01", - "concepts": [ - "AI手机", - "努比亚手机", - "苹果手机产业链", - "端侧AI芯片", - "AI PC", - "AIPC", - "高通概念" - ] - }, - { - "lv3": "AR/VR/MR与智能眼镜", - "lv3_id": "L3-04-01-02", - "concepts": [ - "AI眼镜", - "AR眼镜", - "META智能眼镜", - "MR", - "Rokid AR", - "小米眼镜", - "智能眼镜", - "苹果MR产业链", - "雷鸟创新光波导", - "阿里夸克AI眼镜" - ] - }, - { - "lv3": "智能穿戴", - "lv3_id": "L3-04-01-03", - "concepts": [ - "智能穿戴", - "华为Mate70手表", - "消费电子-玄玑感知系统" - ] - }, - { - "lv3": "其他AI终端", - "lv3_id": "L3-04-01-04", - "concepts": [ - "微泵液冷", - "石墨烯散热", - "云手机" - ] - } - ] - }, - { - "lv2": "创新形态与零部件", - "lv2_id": "L2-04-02", - "concepts": [ - "AI手势识别", - "AI隔空投送", - "AR镀膜", - "ISP视觉", - "华为三折叠屏", - "折叠屏", - "显影液及硅基OLED", - "果链OPEN AI复用", - "苹果供应商核心公司", - "苹果折叠屏", - "苹果OLED潜在受益" - ] - }, - { - "lv2": "华为消费电子生态", - "lv2_id": "L2-04-03", - "concepts": [ - "华为Pura70", - "华为P70", - "华为鸿蒙", - "华为Mate80", - "华为MATE70", - "华为海思星闪", - "鸿蒙PC" - ] - } - ] - }, - { - "lv1": "未来出行与新空间", - "lv1_id": "L1-05", - "children": [ - { - "lv2": "智能驾驶与汽车", - "lv2_id": "L2-05-01", - "children": [ - { - "lv3": "自动驾驶(L4及以上)", - "lv3_id": "L3-05-01-01", - "concepts": [ - "Robotaxi", - "京东物流Robovan", - "小马智行", - "无人环卫车", - "无人物流", - "文远知行", - "无人物流车九识智能", - "无人驾驶", - "特斯拉无人驾驶出租车Robotaxi", - "特斯拉RoboTaxi概念", - "菜鸟无人物流车", - "无人驾驶公交", - "矿山智驾", - "自动驾驶" - ] - }, - { - "lv3": "智能驾驶(L2-L3)", - "lv3_id": "L3-05-01-02", - "concepts": [ - "特斯拉FSD", - "地平线概念", - "地平线", - "智能驾驶产业链", - "比亚迪智驾", - "禾赛科技概念股" - ] - }, - { - "lv3": "整车与生态", - "lv3_id": "L3-05-01-03", - "concepts": [ - "特斯拉产业链", - "小米汽车产业链", - "小米汽车产业链弹性", - "小米概念", - "小米汽车产业链弹性测算", - "小米YU7供应链弹性测算", - "比亚迪产业链", - "上汽集团华为智行", - "奇瑞汽车", - "荣耀股改" - ] - }, - { - "lv3": "汽车电子与零部件", - "lv3_id": "L3-05-01-04", - "concepts": [ - "空中成像", - "汽车无线充电", - "汽车安全" - ] - } - ] - }, - { - "lv2": "低空经济与飞行汽车", - "lv2_id": "L2-05-02", - "concepts": [ - "长安飞行汽车机器人概念", - "eVTOL材料", - "亿航智能订单量", - "低空经济亿航智能", - "低空管控", - "低空物流", - "低空经济&飞行汽车", - "低空设计", - "低空经济", - "低空经济产业链汇集", - "小鹏汇天", - "小鹏汇天供应商", - "飞行汽车eVTOL" - ] - }, - { - "lv2": "商业航天与卫星通信", - "lv2_id": "L2-05-03", - "concepts": [ - "6G概念", - "低轨卫星通信华为", - "凌空天行", - "北斗信使", - "北斗导航", - "卫星互联网", - "卫星出海", - "商业航天卫星通信", - "商业航天", - "太空旅行", - "太空行走", - "太空算力", - "手机直连卫星", - "星河动力", - "星网", - "蓝箭航天朱雀三号" - ] - }, - { - "lv2": "车路协同", - "lv2_id": "L2-05-04", - "concepts": [ - "车路云-车路协同运营建设", - "车路协同", - "车路云一体化" - ] - } - ] - }, - { - "lv1": "新能源与电力", - "lv1_id": "L1-06", - "children": [ - { - "lv2": "新能源技术", - "lv2_id": "L2-06-01", - "children": [ - { - "lv3": "光伏", - "lv3_id": "L3-06-01-01", - "concepts": [ - "N型产业链", - "光伏", - "光伏行业兼并重组", - "光伏产业链", - "反内卷光伏", - "叠层钙钛矿", - "钙钛矿电池" - ] - }, - { - "lv3": "风电", - "lv3_id": "L3-06-01-02", - "concepts": [ - "海上风电", - "风电" - ] - }, - { - "lv3": "储能与电池技术", - "lv3_id": "L3-06-01-03", - "concepts": [ - "固态电池设备", - "固态电池-硅基负极", - "固态电池-硫化物", - "固态电池负极集流体材料-铜箔", - "固态电池", - "固态电池产业链", - "复合集流体", - "富锂锰基材料", - "有机框架材料", - "硅基负极材料", - "钠离子电池", - "陶瓷隔膜骨架膜", - "储能", - "电化学储能系统" - ] - }, - { - "lv3": "核能", - "lv3_id": "L3-06-01-04", - "concepts": [ - "可控核聚变", - "微型核电", - "核污染防治", - "核聚变超导", - "核电钍基熔盐堆", - "核电产业链", - "高温概念" - ] - } - ] - }, - { - "lv2": "电力设备与电网", - "lv2_id": "L2-06-02", - "concepts": [ - "变压器出海", - "燃气轮机HRSG", - "电力产业链", - "电力电网", - "电力", - "电力设备", - "智能电表" - ] - }, - { - "lv2": "充电与换电", - "lv2_id": "L2-06-03", - "concepts": [ - "换电重卡", - "换电", - "充电桩", - "华为智能充电", - "华为智能充电网络", - "比亚迪兆瓦闪充" - ] - } - ] - }, - { - "lv1": "周期性行业与资源品", - "lv1_id": "L1-07", - "children": [ - { - "lv2": "化工", - "lv2_id": "L2-07-01", - "concepts": [ - "H酸活性染料", - "TMA偏苯三酸酐", - "六氟磷酸锂", - "光引发剂", - "农药杀虫剂-氯虫苯甲酰胺", - "化工品涨价", - "化工概念", - "化工", - "双季戊四醇", - "己内酰胺", - "有机染料", - "有机硅", - "有机硅产业链", - "泰乐菌素原料药", - "正丙醇", - "涤纶长丝", - "烧碱", - "甲苯二异氰酸酯-TDI", - "电解液产业链", - "环氧丙烷", - "电解液添加剂", - "纯碱", - "维生素", - "磷化工六氟磷酸锂", - "苯酚丙酮", - "聚酯产业", - "磷化工", - "隔膜", - "除草剂-烯草酮" - ] - }, - { - "lv2": "金属与矿产", - "lv2_id": "L2-07-02", - "concepts": [ - "铜", - "铜产业", - "电解铝", - "锂资源", - "白银", - "出口管制", - "化工有色元素周期表", - "缅甸地震", - "稀土", - "钨金属", - "钴金属", - "钼金属", - "锡矿", - "钴" - ] - }, - { - "lv2": "航运", - "lv2_id": "L2-07-03", - "concepts": [ - "以伊冲突-天然气", - "以伊冲突-航运", - "以伊冲突-油运仓储", - "航运", - "远洋航运" - ] - }, - { - "lv2": "能源", - "lv2_id": "L2-07-04", - "concepts": [ - "以伊冲突-资源化工", - "油气", - "石油" - ] - }, - { - "lv2": "其他周期", - "lv2_id": "L2-07-05", - "concepts": [ - "面板", - "工程机械", - "家用电器" - ] - } - ] - }, - { - "lv1": "宏观政策与金融", - "lv1_id": "L1-08", - "children": [ - { - "lv2": "国企改革与市值管理", - "lv2_id": "L2-08-01", - "concepts": [ - "IPO终止相关企业重组预期", - "中兵集团并购重组", - "上海并购重组", - "安徽国资", - "宝德计算机", - "并购重组预期", - "并购重组", - "整车央企重组", - "消费医疗重组预期", - "河南国资能源集团重组", - "珠海国资", - "科创板并购重组", - "科技重组", - "超聚变借壳预期", - "重组-中科院系&海光系", - "中字头央企", - "中字头", - "国资高息股", - "央国企地产", - "央企市值管理", - "央国企重组", - "央国企", - "市值管理16条-破净股", - "破净央国企", - "破净股合集", - "高分红预期", - "高股息合集" - ] - }, - { - "lv2": "地缘政治与贸易", - "lv2_id": "L2-08-02", - "concepts": [ - "华为", - "东盟贸易", - "中俄贸易", - "中美关系", - "中欧贸易", - "二轮车全地形车", - "乙烷", - "关税豁免", - "关税减免出口链", - "反制关税涨价预期", - "后关税战受益", - "墨西哥汽车零部件", - "外贸出口", - "对日反制", - "海事反制", - "绒毛浆", - "芬太尼管制", - "越南工厂", - "金霉素", - "转口贸易出口转内销" - ] - }, - { - "lv2": "区域发展与基建", - "lv2_id": "L2-08-03", - "concepts": [ - "三峡水运新通道", - "新疆概念", - "新藏铁路", - "水利工程", - "水利", - "混凝土减水剂、砂石设备", - "节水产业240423", - "西南水电站-机构测算", - "西南水电站", - "西南水电", - "西部大开发240424", - "西部大开发", - "重庆", - "隧洞设备盾构机", - "雅下水电站大件物流", - "雅下水电站", - "雅下水电对电力设备增量测算-机构", - "上海自贸区", - "海南", - "海南自贸区", - "海南自贸港" - ] - }, - { - "lv2": "金融改革", - "lv2_id": "L2-08-04", - "concepts": [ - "AMC中央汇金", - "AMC", - "券商合并预期", - "国有大行定增一览", - "大金融", - "湘财合并大智慧", - "证券" - ] - }, - { - "lv2": "产业与社会政策", - "lv2_id": "L2-08-05", - "concepts": [ - "新质生产力", - "工业设备更新", - "设备更新", - "反内卷", - "反内卷食用盐", - "反内卷造纸", - "反内卷合集", - "反内卷快递", - "涨价概念", - "城市旧改", - "城市更新", - "城市更新电梯", - "房屋检测", - "房地产产业链" - ] - } - ] - }, - { - "lv1": "数字内容与消费", - "lv1_id": "L1-09", - "children": [ - { - "lv2": "AIGC与数字内容", - "lv2_id": "L2-09-01", - "concepts": [ - "AI游戏", - "SORA概念", - "内容审核概念", - "国产游戏黑神话", - "影视", - "影视IP", - "幻兽帕鲁", - "影视传媒", - "文生视频", - "游戏", - "版权", - "漫剧", - "短剧", - "腾讯短剧重点名单", - "智象未来" - ] - }, - { - "lv2": "IP经济与新消费", - "lv2_id": "L2-09-02", - "concepts": [ - "诡秘之主", - "上市潮玩盲盒公司", - "卡游文创玩具", - "布鲁可IP衍生品", - "布鲁可谷子经济", - "泡泡玛特概念", - "潮玩产业", - "蜜雪冰城", - "谷子商城", - "积木玩具10大", - "谷子经济" - ] - }, - { - "lv2": "平台经济", - "lv2_id": "L2-09-03", - "concepts": [ - "华为抖音支付", - "华为鸿蒙甄选与支付", - "TikTok", - "小红书概念", - "小红书概念股", - "抖音概念" - ] - }, - { - "lv2": "体育、文旅与服务消费", - "lv2_id": "L2-09-04", - "concepts": [ - "体育", - "体育产业", - "川超联赛", - "第十五届全运会", - "荒野求生", - "足球", - "足球-苏超联赛、体彩", - "冰雪经济", - "大消费", - "文旅旅游消费", - "旅游", - "旅游消费" - ] - }, - { - "lv2": "生物医药与养老", - "lv2_id": "L2-09-05", - "concepts": [ - "创新药双抗", - "创新药相关", - "创新药", - "医疗器械", - "医药", - "三胎", - "多胎", - "多胎辅助生殖概念240926", - "学前教育", - "育儿补贴", - "辅助生殖", - "养老机器人", - "个人养老金", - "养老概念", - "银发经济" - ] - } - ] - }, - { - "lv1": "国防军工", - "lv1_id": "L1-10", - "children": [ - { - "lv2": "航空航天装备", - "lv2_id": "L2-10-01", - "concepts": [ - "军机", - "国产航母", - "地面兵装", - "巴黎航展", - "海军", - "珠海航展", - "电磁弹射概念股", - "电磁发射设备", - "航母福建舰240430" - ] - }, - { - "lv2": "信息化与无人作战", - "lv2_id": "L2-10-02", - "concepts": [ - "AI军工", - "九天无人机", - "信息支援概念整理", - "军工水面水下作战", - "军工信息化", - "军用无人机反无人机", - "无人机蜂群", - "水下军工", - "远程火力" - ] - }, - { - "lv2": "军工综合与军贸", - "lv2_id": "L2-10-03", - "concepts": [ - "军工", - "军工-阅兵", - "军贸", - "国防军工", - "巴印军贸" - ] - } - ] - }, - { - "lv1": "信息技术与安全", - "lv1_id": "L1-11", - "children": [ - { - "lv2": "信创与自主可控", - "lv2_id": "L2-11-01", - "concepts": [ - "信创概念", - "关键软件", - "国产信创概览", - "工业软件", - "政务云政务IT", - "自主可控", - "软件自主可控" - ] - }, - { - "lv2": "数据要素与数字基建", - "lv2_id": "L2-11-02", - "concepts": [ - "地理信息", - "RDA概念股", - "RWA上链— IoT设备数据采集", - "数据可信", - "数据要素", - "文化数据资产交易", - "数据维护", - "数据交易所", - "跨境数据数据要素" - ] - }, - { - "lv2": "金融科技与数字货币", - "lv2_id": "L2-11-03", - "concepts": [ - "上海浦江数链", - "复星稳定币", - "树图链概念", - "稳定币-蚂蚁国际", - "稳定币一体机", - "稳定币RWA概念股", - "香港金融牌照", - "CIPS", - "数字货币", - "跨境支付", - "金砖支付概念" - ] - }, - { - "lv2": "通信技术与网络安全", - "lv2_id": "L2-11-04", - "concepts": [ - "5.5G", - "5G-A", - "5G毫米波", - "eSIM概念", - "IPV6", - "华为5G", - "通感一体", - "充电宝", - "信息安全", - "安全概念股", - "通信安全", - "通信设备" - ] - } - ] - }, - { - "lv1": "其他主题", - "lv1_id": "L1-12", - "children": [ - { - "lv2": "事件驱动与Meme", - "lv2_id": "L2-12-01", - "concepts": [ - "孙潇雅团队概念股", - "小鹏产业链", - "燃气设备", - "“马”字辈", - "低价股", - "周杰伦概念股", - "章盟主概念股", - "韦神概念股", - "高送转概念股" - ] - } - ] - } - ], - "uncategorized": [] -} \ No newline at end of file diff --git a/src/views/Concept/index.js b/src/views/Concept/index.js index bef70f23..14dc571d 100644 --- a/src/views/Concept/index.js +++ b/src/views/Concept/index.js @@ -1693,8 +1693,7 @@ const ConceptCenter = () => { - - + {searchQuery && sortBy === '_score' && (