diff --git a/backend/app/routers/config.py b/backend/app/routers/config.py index 0c3846f..772c4df 100644 --- a/backend/app/routers/config.py +++ b/backend/app/routers/config.py @@ -1,7 +1,6 @@ -""" -API router for configuration management -""" from fastapi import APIRouter, Depends, HTTPException +from typing import Dict, Any, Optional +from pydantic import BaseModel from app.core.dependencies import get_config_manager from app.schemas.config import ConfigResponse, ConfigUpdateRequest, ConfigTestRequest, ConfigTestResponse @@ -9,11 +8,112 @@ from app.services.config_manager import ConfigManager router = APIRouter() +class LLMConfigUpdate(BaseModel): + provider: str + model: Optional[str] = None + @router.get("/", response_model=ConfigResponse) async def get_config(config_manager: ConfigManager = Depends(get_config_manager)): """Retrieve the current system configuration.""" return await config_manager.get_config() +@router.get("/llm", response_model=Dict[str, Any]) +async def get_llm_config(config_manager: ConfigManager = Depends(get_config_manager)): + """Get LLM provider and model configuration.""" + llm_config = await config_manager.get_llm_config() + return llm_config + +@router.put("/llm", response_model=Dict[str, Any]) +async def update_llm_config( + llm_update: LLMConfigUpdate, + config_manager: ConfigManager = Depends(get_config_manager) +): + """Update LLM provider and model configuration.""" + import json + import os + + provider = llm_update.provider + model = llm_update.model + + # Load base config + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) + config_path = os.path.join(project_root, "config", "config.json") + + base_config = {} + if os.path.exists(config_path): + try: + with open(config_path, "r", encoding="utf-8") as f: + base_config = json.load(f) + except Exception: + pass + + # Update llm config + if "llm" not in base_config: + base_config["llm"] = {} + + base_config["llm"]["provider"] = provider + if model: + # Update model in the provider-specific config + if provider in base_config["llm"]: + if not isinstance(base_config["llm"][provider], dict): + base_config["llm"][provider] = {} + base_config["llm"][provider]["model"] = model + + # Save to file + try: + with open(config_path, "w", encoding="utf-8") as f: + json.dump(base_config, f, ensure_ascii=False, indent=2) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save config: {str(e)}") + + # Also update in database - use same error handling as _load_dynamic_config_from_db + try: + from app.models.system_config import SystemConfig + from sqlalchemy.future import select + + result = await config_manager.db.execute( + select(SystemConfig).where(SystemConfig.config_key == "llm") + ) + existing_llm_config = result.scalar_one_or_none() + + if existing_llm_config: + if isinstance(existing_llm_config.config_value, dict): + existing_llm_config.config_value["provider"] = provider + if model: + if provider not in existing_llm_config.config_value: + existing_llm_config.config_value[provider] = {} + elif not isinstance(existing_llm_config.config_value[provider], dict): + existing_llm_config.config_value[provider] = {} + existing_llm_config.config_value[provider]["model"] = model + else: + existing_llm_config.config_value = {"provider": provider} + if model: + existing_llm_config.config_value[provider] = {"model": model} + else: + new_llm_config = SystemConfig( + config_key="llm", + config_value={"provider": provider} + ) + if model: + new_llm_config.config_value[provider] = {"model": model} + config_manager.db.add(new_llm_config) + + await config_manager.db.commit() + except Exception as e: + # Rollback on error + try: + await config_manager.db.rollback() + except Exception: + pass + + # Log the error but don't fail the request since file was already saved + import logging + logger = logging.getLogger(__name__) + logger.warning(f"Failed to update LLM config in database (file saved successfully): {e}") + # Continue anyway since file config was saved successfully + + return await config_manager.get_llm_config() + @router.put("/", response_model=ConfigResponse) async def update_config( config_update: ConfigUpdateRequest, diff --git a/backend/app/routers/financial.py b/backend/app/routers/financial.py index 544c2b7..b2c94c7 100644 --- a/backend/app/routers/financial.py +++ b/backend/app/routers/financial.py @@ -8,7 +8,7 @@ from datetime import datetime, timezone, timedelta from enum import Enum from typing import Dict, List -from fastapi import APIRouter, HTTPException, Query +from fastapi import APIRouter, HTTPException, Query, Depends from fastapi.responses import StreamingResponse from app.core.config import settings @@ -24,6 +24,9 @@ from app.schemas.financial import ( ) from app.services.company_profile_client import CompanyProfileClient from app.services.analysis_client import AnalysisClient, load_analysis_config, get_analysis_config +from app.core.dependencies import get_config_manager +from app.services.config_manager import ConfigManager +from app.services.client_factory import create_analysis_client # Lazy DataManager loader to avoid import-time failures when optional providers/config are missing _dm = None @@ -92,6 +95,7 @@ async def get_data_sources(): async def generate_full_analysis( ts_code: str, company_name: str = Query(None, description="Company name for better context"), + config_manager: ConfigManager = Depends(get_config_manager), ): """ Generate a full analysis report by orchestrating multiple analysis modules @@ -102,20 +106,11 @@ async def generate_full_analysis( logger.info(f"[API] Full analysis requested for {ts_code}") - # Load base and analysis configurations - base_cfg = _load_json(BASE_CONFIG_PATH) - llm_provider = base_cfg.get("llm", {}).get("provider", "gemini") - llm_config = base_cfg.get("llm", {}).get(llm_provider, {}) - - api_key = llm_config.get("api_key") - base_url = llm_config.get("base_url") - - if not api_key: - logger.error(f"[API] API key for {llm_provider} not configured") - raise HTTPException( - status_code=500, - detail=f"API key for {llm_provider} not configured." - ) + # Load LLM configuration using ConfigManager + llm_config_result = await config_manager.get_llm_config() + default_provider = llm_config_result["provider"] + default_config = llm_config_result["config"] + global_model = llm_config_result.get("model") # 全局模型配置 analysis_config_full = load_analysis_config() modules_config = analysis_config_full.get("analysis_modules", {}) @@ -211,10 +206,15 @@ async def generate_full_analysis( module_config = modules_config[module_type] logger.info(f"[Orchestrator] Starting analysis for module: {module_type}") - client = AnalysisClient( - api_key=api_key, - base_url=base_url, - model=module_config.get("model", "gemini-1.5-flash") + # 统一使用全局配置,不再从模块配置读取 provider 和 model + # 使用全局 provider 和 model + model = global_model or default_config.get("model", "gemini-1.5-flash") + + # Create client using factory with global config + client = create_analysis_client( + provider=default_provider, + config=default_config, + model=model ) # Gather context from completed dependencies @@ -468,6 +468,7 @@ async def get_financials( async def get_company_profile( ts_code: str, company_name: str = Query(None, description="Company name for better context"), + config_manager: ConfigManager = Depends(get_config_manager), ): """ Get company profile for a company using Gemini AI (non-streaming, single response) @@ -477,19 +478,26 @@ async def get_company_profile( logger.info(f"[API] Company profile requested for {ts_code}") - # Load config - base_cfg = _load_json(BASE_CONFIG_PATH) - llm_provider = base_cfg.get("llm", {}).get("provider", "gemini") - llm_config = base_cfg.get("llm", {}).get(llm_provider, {}) + # Load LLM configuration using ConfigManager + llm_config_result = await config_manager.get_llm_config() + provider = llm_config_result["provider"] + provider_config = llm_config_result["config"] - api_key = llm_config.get("api_key") - base_url = llm_config.get("base_url") # Will be None if not set, handled by client + # CompanyProfileClient only supports OpenAI-compatible APIs + if provider == "alpha_engine": + raise HTTPException( + status_code=400, + detail="Company profile generation does not support AlphaEngine provider. Please use OpenAI-compatible API." + ) + + api_key = provider_config.get("api_key") + base_url = provider_config.get("base_url") if not api_key: - logger.error(f"[API] API key for {llm_provider} not configured") + logger.error(f"[API] API key for {provider} not configured") raise HTTPException( status_code=500, - detail=f"API key for {llm_provider} not configured." + detail=f"API key for {provider} not configured." ) client = CompanyProfileClient( @@ -573,6 +581,7 @@ async def generate_analysis( ts_code: str, analysis_type: str, company_name: str = Query(None, description="Company name for better context"), + config_manager: ConfigManager = Depends(get_config_manager), ): """ Generate analysis for a company using Gemini AI @@ -591,37 +600,11 @@ async def generate_analysis( logger.info(f"[API] Analysis requested for {ts_code}, type: {analysis_type}") - # Load config - base_cfg = _load_json(BASE_CONFIG_PATH) - llm_provider = base_cfg.get("llm", {}).get("provider", "gemini") - llm_config = base_cfg.get("llm", {}).get(llm_provider, {}) - - api_key = llm_config.get("api_key") - base_url = llm_config.get("base_url") - - if not api_key: - logger.error(f"[API] API key for {llm_provider} not configured") - raise HTTPException( - status_code=500, - detail=f"API key for {llm_provider} not configured." - ) - - # Get analysis configuration - analysis_cfg = get_analysis_config(analysis_type) - if not analysis_cfg: - raise HTTPException( - status_code=404, - detail=f"Analysis type '{analysis_type}' not found in configuration" - ) - - model = analysis_cfg.get("model", "gemini-2.5-flash") - prompt_template = analysis_cfg.get("prompt_template", "") - - if not prompt_template: - raise HTTPException( - status_code=500, - detail=f"Prompt template not found for analysis type '{analysis_type}'" - ) + # Load LLM configuration using ConfigManager + llm_config_result = await config_manager.get_llm_config() + default_provider = llm_config_result["provider"] + default_config = llm_config_result["config"] + global_model = llm_config_result.get("model") # 全局模型配置 # Get company name from ts_code if not provided financial_data = None @@ -656,8 +639,30 @@ async def generate_analysis( logger.info(f"[API] Generating {analysis_type} for {company_name}") - # Initialize analysis client with configured model - client = AnalysisClient(api_key=api_key, base_url=base_url, model=model) + # Get analysis configuration for prompt template + analysis_cfg = get_analysis_config(analysis_type) + if not analysis_cfg: + raise HTTPException( + status_code=404, + detail=f"Analysis type '{analysis_type}' not found in configuration" + ) + + prompt_template = analysis_cfg.get("prompt_template", "") + if not prompt_template: + raise HTTPException( + status_code=500, + detail=f"Prompt template not found for analysis type '{analysis_type}'" + ) + + # 统一使用全局配置,不再从模块配置读取 provider 和 model + model = global_model or default_config.get("model", "gemini-1.5-flash") + + # 统一使用全局配置创建客户端 + client = create_analysis_client( + provider=default_provider, + config=default_config, + model=model + ) # Prepare dependency context for single-module generation # If the requested module declares dependencies, generate them first and inject their outputs @@ -701,12 +706,18 @@ async def generate_analysis( # Fallback: if cycle detected, just use any order order = list(all_required) - # Generate dependencies in order + # Generate dependencies in order - 统一使用全局配置 completed = {} for mod in order: cfg = modules_config.get(mod, {}) dep_ctx = {d: completed.get(d, "") for d in (cfg.get("dependencies", []) or [])} - dep_client = AnalysisClient(api_key=api_key, base_url=base_url, model=cfg.get("model", model)) + + # 统一使用全局配置,不再从模块配置读取 + dep_client = create_analysis_client( + provider=default_provider, + config=default_config, + model=model + ) dep_result = await dep_client.generate_analysis( analysis_type=mod, company_name=company_name, @@ -888,6 +899,7 @@ async def stream_analysis( ts_code: str, analysis_type: str, company_name: str = Query(None, description="Company name for better context"), + config_manager: ConfigManager = Depends(get_config_manager), ): """ Stream analysis content chunks for a given module using OpenAI-compatible streaming. @@ -899,24 +911,19 @@ async def stream_analysis( logger.info(f"[API] Streaming analysis requested for {ts_code}, type: {analysis_type}") - # Load config - base_cfg = _load_json(BASE_CONFIG_PATH) - llm_provider = base_cfg.get("llm", {}).get("provider", "gemini") - llm_config = base_cfg.get("llm", {}).get(llm_provider, {}) - - api_key = llm_config.get("api_key") - base_url = llm_config.get("base_url") - - if not api_key: - logger.error(f"[API] API key for {llm_provider} not configured") - raise HTTPException(status_code=500, detail=f"API key for {llm_provider} not configured.") + # Load LLM configuration using ConfigManager + llm_config_result = await config_manager.get_llm_config() + default_provider = llm_config_result["provider"] + default_config = llm_config_result["config"] + global_model = llm_config_result.get("model") # 全局模型配置 # Get analysis configuration analysis_cfg = get_analysis_config(analysis_type) if not analysis_cfg: raise HTTPException(status_code=404, detail=f"Analysis type '{analysis_type}' not found in configuration") - model = analysis_cfg.get("model", "gemini-2.5-flash") + # 统一使用全局配置,不再从模块配置读取 provider 和 model + model = global_model or default_config.get("model", "gemini-1.5-flash") prompt_template = analysis_cfg.get("prompt_template", "") if not prompt_template: raise HTTPException(status_code=500, detail=f"Prompt template not found for analysis type '{analysis_type}'") @@ -972,7 +979,13 @@ async def stream_analysis( for mod in order: cfg = modules_config.get(mod, {}) dep_ctx = {d: completed.get(d, "") for d in (cfg.get("dependencies", []) or [])} - dep_client = AnalysisClient(api_key=api_key, base_url=base_url, model=cfg.get("model", model)) + + # 统一使用全局配置,不再从模块配置读取 + dep_client = create_analysis_client( + provider=default_provider, + config=default_config, + model=model + ) dep_result = await dep_client.generate_analysis( analysis_type=mod, company_name=company_name, @@ -986,7 +999,12 @@ async def stream_analysis( except Exception: context = {} - client = AnalysisClient(api_key=api_key, base_url=base_url, model=model) + # 统一使用全局配置创建客户端 + client = create_analysis_client( + provider=default_provider, + config=default_config, + model=model + ) async def streamer(): # Optional header line to help client-side UI diff --git a/backend/app/routers/orgs.py b/backend/app/routers/orgs.py index 252bfcc..dac814e 100644 --- a/backend/app/routers/orgs.py +++ b/backend/app/routers/orgs.py @@ -2,7 +2,7 @@ import logging import os import json from typing import Dict -from fastapi import APIRouter, BackgroundTasks, HTTPException +from fastapi import APIRouter, BackgroundTasks, HTTPException, Depends # Lazy loader for DataManager _dm = None @@ -23,6 +23,9 @@ def get_dm(): return _dm from app.services.analysis_client import AnalysisClient, load_analysis_config +from app.core.dependencies import get_config_manager +from app.services.config_manager import ConfigManager +from app.services.client_factory import create_analysis_client router = APIRouter() logger = logging.getLogger(__name__) @@ -40,7 +43,7 @@ def _load_json(path: str) -> Dict: except Exception: return {} -async def run_full_analysis(org_id: str): +async def run_full_analysis(org_id: str, config_manager: ConfigManager = None): """ Asynchronous task to run a full analysis for a given stock. This function is market-agnostic and relies on DataManager. @@ -48,16 +51,23 @@ async def run_full_analysis(org_id: str): logger.info(f"Starting full analysis task for {org_id}") # 1. Load configurations - base_cfg = _load_json(BASE_CONFIG_PATH) - llm_provider = base_cfg.get("llm", {}).get("provider", "gemini") - llm_config = base_cfg.get("llm", {}).get(llm_provider, {}) - - api_key = llm_config.get("api_key") - base_url = llm_config.get("base_url") + if config_manager is None: + # If called from background task, we need to create a new session + from app.core.database import AsyncSessionLocal + async with AsyncSessionLocal() as session: + config_manager = ConfigManager(db_session=session) + await _run_analysis_with_config(org_id, config_manager) + else: + await _run_analysis_with_config(org_id, config_manager) - if not api_key: - logger.error(f"API key for {llm_provider} not configured. Aborting analysis for {org_id}.") - return + +async def _run_analysis_with_config(org_id: str, config_manager: ConfigManager): + """Internal function to run analysis with a ConfigManager instance""" + # Load LLM configuration using ConfigManager + llm_config_result = await config_manager.get_llm_config() + default_provider = llm_config_result["provider"] + default_config = llm_config_result["config"] + global_model = llm_config_result.get("model") # 全局模型配置 analysis_config_full = load_analysis_config() modules_config = analysis_config_full.get("analysis_modules", {}) @@ -96,10 +106,15 @@ async def run_full_analysis(org_id: str): analysis_results = {} for module_type, module_config in modules_config.items(): logger.info(f"Running analysis module: {module_type} for {org_id}") - client = AnalysisClient( - api_key=api_key, - base_url=base_url, - model=module_config.get("model", "gemini-1.5-flash") + + # 统一使用全局配置,不再从模块配置读取 provider 和 model + model = global_model or default_config.get("model", "gemini-1.5-flash") + + # Create client using factory with global config + client = create_analysis_client( + provider=default_provider, + config=default_config, + model=model ) # Simplified context: use results from all previously completed modules @@ -128,7 +143,7 @@ async def run_full_analysis(org_id: str): @router.post("/{market}/{org_id}/reports/generate") -async def trigger_report_generation(market: str, org_id: str, background_tasks: BackgroundTasks): +async def trigger_report_generation(market: str, org_id: str, background_tasks: BackgroundTasks, config_manager: ConfigManager = Depends(get_config_manager)): """ Triggers a background task to generate a full financial report. This endpoint is now market-agnostic. @@ -137,7 +152,8 @@ async def trigger_report_generation(market: str, org_id: str, background_tasks: # TODO: Create a report record in the database with "generating" status here. - background_tasks.add_task(run_full_analysis, org_id) + # Pass config_manager to the background task + background_tasks.add_task(run_full_analysis, org_id, config_manager) logger.info(f"Queued analysis task for {org_id}.") return {"queued": True, "market": market, "org_id": org_id} diff --git a/backend/app/schemas/config.py b/backend/app/schemas/config.py index 298e0c2..b98a6f7 100644 --- a/backend/app/schemas/config.py +++ b/backend/app/schemas/config.py @@ -1,7 +1,7 @@ """ Configuration-related Pydantic schemas """ -from typing import Dict, Optional, Any +from typing import Dict, Optional, Any, List from pydantic import BaseModel, Field class DatabaseConfig(BaseModel): @@ -11,17 +11,30 @@ class NewApiConfig(BaseModel): api_key: str = Field(..., description="New API Key") base_url: Optional[str] = None +class AlphaEngineConfig(BaseModel): + api_url: str = Field(..., description="AlphaEngine API URL") + api_key: str = Field(..., description="AlphaEngine API Key") + token: str = Field(..., description="AlphaEngine Token") + user_id: int = Field(999041, description="User ID") + model: str = Field("deepseek-r1", description="Model name") + using_indicator: bool = Field(True, description="Whether to use indicators") + start_time: str = Field("2024-01-01", description="Start time for data query") + doc_show_type: List[str] = Field(["A001", "A002", "A003", "A004"], description="Document types") + simple_tracking: bool = Field(True, description="Whether to enable simple tracking") + class DataSourceConfig(BaseModel): api_key: str = Field(..., description="数据源API Key") class ConfigResponse(BaseModel): database: DatabaseConfig new_api: NewApiConfig + alpha_engine: Optional[AlphaEngineConfig] = None data_sources: Dict[str, DataSourceConfig] class ConfigUpdateRequest(BaseModel): database: Optional[DatabaseConfig] = None new_api: Optional[NewApiConfig] = None + alpha_engine: Optional[AlphaEngineConfig] = None data_sources: Optional[Dict[str, DataSourceConfig]] = None class ConfigTestRequest(BaseModel): diff --git a/backend/app/services/alpha_engine_client.py b/backend/app/services/alpha_engine_client.py new file mode 100644 index 0000000..fd77098 --- /dev/null +++ b/backend/app/services/alpha_engine_client.py @@ -0,0 +1,260 @@ +""" +AlphaEngine Client for investment Q&A API +""" +import json +import re +import time +from typing import Dict, Optional, AsyncGenerator +import httpx +from requests.exceptions import ChunkedEncodingError + + +class AlphaEngineClient: + """Client for AlphaEngine investment Q&A API""" + + def __init__( + self, + api_url: str, + api_key: str, + token: str, + user_id: int = 999041, + model: str = "deepseek-r1", + using_indicator: bool = True, + start_time: str = "2024-01-01", + doc_show_type: list = None, + simple_tracking: bool = True + ): + """ + Initialize AlphaEngine client + + Args: + api_url: API endpoint URL + api_key: X-API-KEY for authentication + token: Token for authentication + user_id: User ID + model: Model name (default: deepseek-r1) + using_indicator: Whether to use indicators + start_time: Start time for data query + doc_show_type: Document types to show (default: ["A001", "A002", "A003", "A004"]) + simple_tracking: Whether to enable simple tracking + """ + self.api_url = api_url.rstrip('/') + self.api_key = api_key + self.token = token + self.user_id = user_id + self.model = model + self.using_indicator = using_indicator + self.start_time = start_time + self.doc_show_type = doc_show_type or ["A001", "A002", "A003", "A004"] + self.simple_tracking = simple_tracking + + async def generate_analysis( + self, + analysis_type: str, + company_name: str, + ts_code: str, + prompt_template: str, + financial_data: Optional[Dict] = None, + context: Optional[Dict] = None + ) -> Dict: + """ + Generate analysis using AlphaEngine API (non-streaming) + + Args: + analysis_type: Type of analysis + company_name: Company name + ts_code: Stock code + prompt_template: Prompt template with placeholders + financial_data: Optional financial data for context + context: Optional dictionary with results from previous analyses + + Returns: + Dict with analysis content and metadata + """ + start_time = time.perf_counter_ns() + + # Build prompt from template + prompt = self._build_prompt( + prompt_template, + company_name, + ts_code, + financial_data, + context + ) + + # Call AlphaEngine API + try: + async with httpx.AsyncClient(timeout=300.0) as client: + headers = { + 'token': self.token, + 'X-API-KEY': self.api_key, + 'Content-Type': 'application/json' + } + + payload = { + "msg": prompt, + "history": [], + "user_id": self.user_id, + "model": self.model, + "using_indicator": self.using_indicator, + "start_time": self.start_time, + "doc_show_type": self.doc_show_type, + "simple_tracking": self.simple_tracking + } + + response = await client.post( + f"{self.api_url}/api/v3/finchat", + json=payload, + headers=headers + ) + + if response.status_code != 200: + raise Exception(f"AlphaEngine API error: HTTP {response.status_code} - {response.text}") + + result_text = response.text + + # Parse response to extract final answer + final_answer_match = re.findall(r'\{"id":"_final","content":"(.*?)"}', result_text) + final_answer = final_answer_match[0] if final_answer_match else result_text + + # Extract COT if available + cot_match = re.findall(r'\{"id":"_cot","content":"(.*?)"}', result_text) + cot = "".join(cot_match) if cot_match else "" + + # Extract tracking documents if available + tracking_match = re.findall(r'\{"id":"tracking_documents","content":\s*(\[[^]]*])}', result_text) + tracking_docs = json.loads(tracking_match[0]) if tracking_match else [] + + elapsed_ms = int((time.perf_counter_ns() - start_time) / 1_000_000) + + return { + "content": final_answer, + "model": self.model, + "tokens": { + "prompt_tokens": 0, # AlphaEngine doesn't provide token usage + "completion_tokens": 0, + "total_tokens": 0, + }, + "elapsed_ms": elapsed_ms, + "success": True, + "analysis_type": analysis_type, + "cot": cot, + "tracking_documents": tracking_docs, + } + except Exception as e: + elapsed_ms = int((time.perf_counter_ns() - start_time) / 1_000_000) + return { + "content": "", + "model": self.model, + "tokens": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}, + "elapsed_ms": elapsed_ms, + "success": False, + "error": str(e), + "analysis_type": analysis_type, + } + + async def generate_analysis_stream( + self, + analysis_type: str, + company_name: str, + ts_code: str, + prompt_template: str, + financial_data: Optional[Dict] = None, + context: Optional[Dict] = None + ) -> AsyncGenerator[str, None]: + """ + Yield analysis content chunks using AlphaEngine streaming API + + Yields plain text chunks as they arrive. + """ + # Build prompt + prompt = self._build_prompt( + prompt_template, + company_name, + ts_code, + financial_data, + context, + ) + + try: + async with httpx.AsyncClient(timeout=300.0) as client: + headers = { + 'token': self.token, + 'X-API-KEY': self.api_key, + 'Content-Type': 'application/json' + } + + payload = { + "msg": prompt, + "history": [], + "user_id": self.user_id, + "model": self.model, + "using_indicator": self.using_indicator, + "start_time": self.start_time, + "doc_show_type": self.doc_show_type, + "simple_tracking": self.simple_tracking + } + + async with client.stream( + "POST", + f"{self.api_url}/api/v3/finchat", + json=payload, + headers=headers + ) as response: + if response.status_code != 200: + yield f"\n\n[错误] HTTP {response.status_code}: {response.text}\n" + return + + async for chunk in response.aiter_bytes(chunk_size=128): + try: + chunk_text = chunk.decode('utf-8', 'ignore') + yield chunk_text + except UnicodeDecodeError: + chunk_text = chunk.decode('utf-8', 'replace') + yield chunk_text + except Exception as e: + yield f"\n\n[错误] {type(e).__name__}: {str(e)}\n" + + def _build_prompt( + self, + prompt_template: str, + company_name: str, + ts_code: str, + financial_data: Optional[Dict] = None, + context: Optional[Dict] = None + ) -> str: + """Build prompt from template by replacing placeholders""" + import string + + # Start with base placeholders + placeholders = { + "company_name": company_name, + "ts_code": ts_code, + } + + # Add financial data if provided + financial_data_str = "" + if financial_data: + try: + financial_data_str = json.dumps(financial_data, ensure_ascii=False, indent=2) + except Exception: + financial_data_str = str(financial_data) + placeholders["financial_data"] = financial_data_str + + # Add context from previous analysis steps + if context: + placeholders.update(context) + + # Replace placeholders in template + class SafeFormatter(string.Formatter): + def get_value(self, key, args, kwargs): + if isinstance(key, str): + return kwargs.get(key, f"{{{key}}}") + else: + return super().get_value(key, args, kwargs) + + formatter = SafeFormatter() + prompt = formatter.format(prompt_template, **placeholders) + + return prompt + diff --git a/backend/app/services/client_factory.py b/backend/app/services/client_factory.py new file mode 100644 index 0000000..cccb28c --- /dev/null +++ b/backend/app/services/client_factory.py @@ -0,0 +1,60 @@ +""" +Unified Analysis Client Factory +Creates appropriate client based on provider type +""" +from typing import Dict, Optional +from app.services.analysis_client import AnalysisClient +from app.services.alpha_engine_client import AlphaEngineClient + + +def create_analysis_client( + provider: str, + config: Dict, + model: str = None +): + """ + Create an analysis client based on provider type + + Args: + provider: Provider type ("openai", "gemini", "new_api", "alpha_engine") + config: Configuration dictionary containing provider-specific settings + model: Model name (optional, may be overridden by config) + + Returns: + Client instance (AnalysisClient or AlphaEngineClient) + """ + if provider == "alpha_engine": + # AlphaEngine specific configuration + api_url = config.get("api_url", "") + api_key = config.get("api_key", "") + token = config.get("token", "") + user_id = config.get("user_id", 999041) + model_name = model or config.get("model", "deepseek-r1") + using_indicator = config.get("using_indicator", True) + start_time = config.get("start_time", "2024-01-01") + doc_show_type = config.get("doc_show_type", ["A001", "A002", "A003", "A004"]) + simple_tracking = config.get("simple_tracking", True) + + return AlphaEngineClient( + api_url=api_url, + api_key=api_key, + token=token, + user_id=user_id, + model=model_name, + using_indicator=using_indicator, + start_time=start_time, + doc_show_type=doc_show_type, + simple_tracking=simple_tracking + ) + else: + # OpenAI-compatible API (openai, gemini, new_api) + api_key = config.get("api_key", "") + base_url = config.get("base_url", "") + model_name = model or config.get("model", "gemini-1.5-flash") + + return AnalysisClient( + api_key=api_key, + base_url=base_url, + model=model_name + ) + diff --git a/backend/app/services/company_profile_client.py b/backend/app/services/company_profile_client.py index 676fc5d..15896a6 100644 --- a/backend/app/services/company_profile_client.py +++ b/backend/app/services/company_profile_client.py @@ -68,14 +68,14 @@ class CompanyProfileClient: "error": str(e), } - def generate_profile_stream( + async def generate_profile_stream( self, company_name: str, ts_code: str, financial_data: Optional[Dict] = None ): """ - Generate company profile using Gemini API with streaming + Generate company profile using OpenAI-compatible streaming API Args: company_name: Company name @@ -85,40 +85,31 @@ class CompanyProfileClient: Yields: Chunks of generated content """ - import logging - logger = logging.getLogger(__name__) - - logger.info(f"[CompanyProfile] Starting stream generation for {company_name} ({ts_code})") - # Build prompt prompt = self._build_prompt(company_name, ts_code, financial_data) - logger.info(f"[CompanyProfile] Prompt built, length: {len(prompt)} chars") - # Call Gemini API with streaming + # Call OpenAI-compatible API with streaming try: - logger.info("[CompanyProfile] Calling Gemini API with stream=True") - # Generate streaming response (sync call, but yields chunks) - response_stream = self.model.generate_content(prompt, stream=True) - logger.info("[CompanyProfile] Gemini API stream object created") - - chunk_count = 0 - # Stream chunks - logger.info("[CompanyProfile] Starting to iterate response stream") - for chunk in response_stream: - logger.info(f"[CompanyProfile] Received chunk from Gemini, has text: {hasattr(chunk, 'text')}") - if hasattr(chunk, 'text') and chunk.text: - chunk_count += 1 - text_len = len(chunk.text) - logger.info(f"[CompanyProfile] Chunk {chunk_count}: {text_len} chars") - yield chunk.text - else: - logger.warning(f"[CompanyProfile] Chunk has no text attribute or empty, chunk: {chunk}") - - logger.info(f"[CompanyProfile] Stream iteration completed. Total chunks: {chunk_count}") + stream = await self.client.chat.completions.create( + model=self.model_name, + messages=[{"role": "user", "content": prompt}], + stream=True, + ) + # The SDK yields events with incremental deltas + async for event in stream: + try: + choice = event.choices[0] if getattr(event, "choices", None) else None + delta = getattr(choice, "delta", None) if choice is not None else None + content = getattr(delta, "content", None) if delta is not None else None + if content: + yield content + except Exception: + # Best-effort: ignore malformed chunks + continue except Exception as e: - logger.error(f"[CompanyProfile] Error during streaming: {type(e).__name__}: {str(e)}", exc_info=True) - yield f"\n\n---\n\n**错误**: {type(e).__name__}: {str(e)}" + # Emit error message to the stream so the client can surface it + yield f"\n\n[错误] {type(e).__name__}: {str(e)}\n" def _build_prompt(self, company_name: str, ts_code: str, financial_data: Optional[Dict] = None) -> str: """Build prompt for company profile generation""" diff --git a/backend/app/services/config_manager.py b/backend/app/services/config_manager.py index 27c7af3..f71f2bb 100644 --- a/backend/app/services/config_manager.py +++ b/backend/app/services/config_manager.py @@ -12,7 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from app.models.system_config import SystemConfig -from app.schemas.config import ConfigResponse, ConfigUpdateRequest, DatabaseConfig, NewApiConfig, DataSourceConfig, ConfigTestResponse +from app.schemas.config import ConfigResponse, ConfigUpdateRequest, DatabaseConfig, NewApiConfig, AlphaEngineConfig, DataSourceConfig, ConfigTestResponse class ConfigManager: """Manages system configuration by merging a static JSON file with dynamic settings from the database.""" @@ -71,26 +71,115 @@ class ConfigManager: # 兼容两种位置:优先使用 new_api,其次回退到 llm.new_api new_api_src = merged_config.get("new_api") or merged_config.get("llm", {}).get("new_api", {}) + + # 获取 alpha_engine 配置 + alpha_engine_src = merged_config.get("alpha_engine") or merged_config.get("llm", {}).get("alpha_engine") + alpha_engine_config = None + if alpha_engine_src: + alpha_engine_config = AlphaEngineConfig(**alpha_engine_src) return ConfigResponse( database=DatabaseConfig(**merged_config.get("database", {})), new_api=NewApiConfig(**(new_api_src or {})), + alpha_engine=alpha_engine_config, data_sources={ k: DataSourceConfig(**v) for k, v in merged_config.get("data_sources", {}).items() } ) + + async def get_llm_config(self, provider: str = None) -> Dict[str, Any]: + """ + Get LLM configuration for a specific provider + + Args: + provider: Provider name (e.g., "new_api", "gemini", "alpha_engine") + If None, uses the configured provider from config + + Returns: + Dictionary with provider configuration and provider name + """ + base_config = self._load_base_config_from_file() + db_config = await self._load_dynamic_config_from_db() + + merged_config = self._merge_configs(base_config, db_config) + + llm_config = merged_config.get("llm", {}) + + # Determine provider + if not provider: + provider = llm_config.get("provider", "new_api") + + # Get provider-specific config + provider_config = llm_config.get(provider, {}) + + # Get global model from provider config if available + global_model = provider_config.get("model") + + return { + "provider": provider, + "config": provider_config, + "model": global_model # 返回全局模型配置 + } + + def _filter_empty_values(self, config_dict: Dict[str, Any]) -> Dict[str, Any]: + """Remove empty strings and None values from config dict, but keep 0 and False.""" + filtered = {} + for key, value in config_dict.items(): + if isinstance(value, dict): + filtered_value = self._filter_empty_values(value) + if filtered_value: # Only add if dict is not empty + filtered[key] = filtered_value + elif value is not None and value != "": + filtered[key] = value + return filtered async def update_config(self, config_update: ConfigUpdateRequest) -> ConfigResponse: """Updates configuration in the database and returns the new merged config.""" try: update_dict = config_update.dict(exclude_unset=True) + # 过滤空值 + update_dict = self._filter_empty_values(update_dict) + # 验证配置数据 self._validate_config_data(update_dict) + # 处理 LLM 相关配置:需要保存到 llm 配置下 + llm_updates = {} + if "new_api" in update_dict: + llm_updates["new_api"] = update_dict.pop("new_api") + if "alpha_engine" in update_dict: + llm_updates["alpha_engine"] = update_dict.pop("alpha_engine") + + # 保存 LLM 配置 + if llm_updates: + result = await self.db.execute( + select(SystemConfig).where(SystemConfig.config_key == "llm") + ) + existing_llm_config = result.scalar_one_or_none() + + if existing_llm_config: + if isinstance(existing_llm_config.config_value, dict): + merged_llm = self._merge_configs(existing_llm_config.config_value, llm_updates) + existing_llm_config.config_value = merged_llm + else: + existing_llm_config.config_value = llm_updates + else: + # 从文件加载基础配置,然后合并 + base_config = self._load_base_config_from_file() + base_llm = base_config.get("llm", {}) + merged_llm = self._merge_configs(base_llm, llm_updates) + new_llm_config = SystemConfig(config_key="llm", config_value=merged_llm) + self.db.add(new_llm_config) + + # 保存其他配置(database, data_sources 等) for key, value in update_dict.items(): - existing_config = await self.db.get(SystemConfig, key) + result = await self.db.execute( + select(SystemConfig).where(SystemConfig.config_key == key) + ) + existing_config = result.scalar_one_or_none() + if existing_config: # Merge with existing DB value before updating if isinstance(existing_config.config_value, dict) and isinstance(value, dict): @@ -112,23 +201,32 @@ class ConfigManager: """Validate configuration data before saving.""" if "database" in config_data: db_config = config_data["database"] - if "url" in db_config: + if "url" in db_config and db_config["url"]: url = db_config["url"] if not url.startswith(("postgresql://", "postgresql+asyncpg://")): raise ValueError("数据库URL必须以 postgresql:// 或 postgresql+asyncpg:// 开头") if "new_api" in config_data: new_api_config = config_data["new_api"] - if "api_key" in new_api_config and len(new_api_config["api_key"]) < 10: + if "api_key" in new_api_config and new_api_config["api_key"] and len(new_api_config["api_key"]) < 10: raise ValueError("New API Key长度不能少于10个字符") if "base_url" in new_api_config and new_api_config["base_url"]: base_url = new_api_config["base_url"] if not base_url.startswith(("http://", "https://")): raise ValueError("New API Base URL必须以 http:// 或 https:// 开头") + if "alpha_engine" in config_data: + alpha_engine_config = config_data["alpha_engine"] + if "api_key" in alpha_engine_config and alpha_engine_config["api_key"] and len(alpha_engine_config["api_key"]) < 5: + raise ValueError("AlphaEngine API Key长度不能少于5个字符") + if "api_url" in alpha_engine_config and alpha_engine_config["api_url"]: + api_url = alpha_engine_config["api_url"] + if not api_url.startswith(("http://", "https://")): + raise ValueError("AlphaEngine API URL必须以 http:// 或 https:// 开头") + if "data_sources" in config_data: for source_name, source_config in config_data["data_sources"].items(): - if "api_key" in source_config and len(source_config["api_key"]) < 10: + if "api_key" in source_config and source_config["api_key"] and len(source_config["api_key"]) < 10: raise ValueError(f"{source_name} API Key长度不能少于10个字符") async def test_config(self, config_type: str, config_data: Dict[str, Any]) -> ConfigTestResponse: @@ -142,6 +240,8 @@ class ConfigManager: return await self._test_tushare(config_data) elif config_type == "finnhub": return await self._test_finnhub(config_data) + elif config_type == "alpha_engine": + return await self._test_alpha_engine(config_data) else: return ConfigTestResponse( success=False, @@ -302,3 +402,57 @@ class ConfigManager: success=False, message=f"Finnhub API连接失败: {str(e)}" ) + + async def _test_alpha_engine(self, config_data: Dict[str, Any]) -> ConfigTestResponse: + """Test AlphaEngine API connection.""" + api_url = config_data.get("api_url") + api_key = config_data.get("api_key") + token = config_data.get("token") + + if not api_url or not api_key or not token: + return ConfigTestResponse( + success=False, + message="AlphaEngine API URL、API Key和Token均不能为空" + ) + + try: + async with httpx.AsyncClient(timeout=10.0) as client: + headers = { + 'token': token, + 'X-API-KEY': api_key, + 'Content-Type': 'application/json' + } + + # 发送一个简单的测试请求 + payload = { + "msg": "测试连接", + "history": [], + "user_id": config_data.get("user_id", 999041), + "model": config_data.get("model", "deepseek-r1"), + "using_indicator": config_data.get("using_indicator", True), + "start_time": config_data.get("start_time", "2024-01-01"), + "doc_show_type": config_data.get("doc_show_type", ["A001", "A002", "A003", "A004"]), + "simple_tracking": config_data.get("simple_tracking", True) + } + + response = await client.post( + f"{api_url.rstrip('/')}/api/v3/finchat", + json=payload, + headers=headers + ) + + if response.status_code == 200: + return ConfigTestResponse( + success=True, + message="AlphaEngine API连接成功" + ) + else: + return ConfigTestResponse( + success=False, + message=f"AlphaEngine API测试失败: HTTP {response.status_code} - {response.text[:200]}" + ) + except Exception as e: + return ConfigTestResponse( + success=False, + message=f"AlphaEngine API连接失败: {str(e)}" + ) diff --git a/config/analysis-config.json b/config/analysis-config.json index 402b68e..191f0ed 100644 --- a/config/analysis-config.json +++ b/config/analysis-config.json @@ -2,49 +2,57 @@ "analysis_modules": { "company_profile": { "name": "公司简介", - "model": "qwen-flash-2025-07-28", - "prompt_template": "您是一位专业的证券市场分析师。请为公司 {company_name} (股票代码: {ts_code}) 生成一份详细且专业的公司介绍。开头不要自我介绍,直接开始正文。正文用MarkDown输出,尽量说明信息来源,用斜体显示信息来源。在生成内容时,请严格遵循以下要求并采用清晰、结构化的格式:\n\n1. **公司概览**:\n * 简要介绍公司的性质、核心业务领域及其在行业中的定位。\n * 提炼并阐述公司的核心价值理念。\n\n2. **主营业务**:\n * 详细描述公司主要的**产品或服务**。\n * **重要提示**:如果能获取到公司最新的官方**年报**或**财务报告**,请从中提取各主要产品/服务线的**收入金额**和其占公司总收入的**百分比**。请**明确标注数据来源**(例如:\"数据来源于XX年年度报告\")。\n * **严格禁止**编造或估算任何财务数据。若无法找到公开、准确的财务数据,请**不要**在这一点中提及具体金额或比例,仅描述业务内容。\n\n3. **发展历程**:\n * 以时间线或关键事件的形式,概述公司自成立以来的主要**里程碑事件**、重大发展阶段、战略转型或重要成就。\n\n4. **核心团队**:\n * 介绍公司**主要管理层和核心技术团队成员**。\n * 对于每位核心成员,提供其**职务、主要工作履历、教育背景**。\n * 如果公开可查,可补充其**出生年份**。\n\n5. **供应链**:\n * 描述公司的**主要原材料、部件或服务来源**。\n * 如果公开信息中包含,请列出**主要供应商名称**,并**明确其在总采购金额中的大致占比**。若无此数据,则仅描述采购模式。\n\n6. **主要客户及销售模式**:\n * 阐明公司的**销售模式**(例如:直销、经销、线上销售、代理等)。\n * 列出公司的**主要客户群体**或**代表性大客户**。\n * 如果公开信息中包含,请标明**主要客户(或前五大客户)的销售额占公司总销售额的比例**。若无此数据,则仅描述客户类型。\n\n7. **未来展望**:\n * 基于公司**公开的官方声明、管理层访谈或战略规划**,总结公司未来的发展方向、战略目标、重点项目或市场预期。请确保此部分内容有可靠的信息来源支持。" + "model": "deepseek-r1", + "prompt_template": "您是一位专业的证券市场分析师。请为公司 {company_name} (股票代码: {ts_code}) 生成一份详细且专业的公司介绍。开头不要自我介绍,直接开始正文。正文用MarkDown输出,尽量说明信息来源,用斜体显示信息来源。在生成内容时,请严格遵循以下要求并采用清晰、结构化的格式:\n\n1. **公司概览**:\n * 简要介绍公司的性质、核心业务领域及其在行业中的定位。\n * 提炼并阐述公司的核心价值理念。\n\n2. **主营业务**:\n * 详细描述公司主要的**产品或服务**。\n * **重要提示**:如果能获取到公司最新的官方**年报**或**财务报告**,请从中提取各主要产品/服务线的**收入金额**和其占公司总收入的**百分比**。请**明确标注数据来源**(例如:\"数据来源于XX年年度报告\")。\n * **严格禁止**编造或估算任何财务数据。若无法找到公开、准确的财务数据,请**不要**在这一点中提及具体金额或比例,仅描述业务内容。\n\n3. **发展历程**:\n * 以时间线或关键事件的形式,概述公司自成立以来的主要**里程碑事件**、重大发展阶段、战略转型或重要成就。\n\n4. **核心团队**:\n * 介绍公司**主要管理层和核心技术团队成员**。\n * 对于每位核心成员,提供其**职务、主要工作履历、教育背景**。\n * 如果公开可查,可补充其**出生年份**。\n\n5. **供应链**:\n * 描述公司的**主要原材料、部件或服务来源**。\n * 如果公开信息中包含,请列出**主要供应商名称**,并**明确其在总采购金额中的大致占比**。若无此数据,则仅描述采购模式。\n\n6. **主要客户及销售模式**:\n * 阐明公司的**销售模式**(例如:直销、经销、线上销售、代理等)。\n * 列出公司的**主要客户群体**或**代表性大客户**。\n * 如果公开信息中包含,请标明**主要客户(或前五大客户)的销售额占公司总销售额的比例**。若无此数据,则仅描述客户类型。\n\n7. **未来展望**:\n * 基于公司**公开的官方声明、管理层访谈或战略规划**,总结公司未来的发展方向、战略目标、重点项目或市场预期。请确保此部分内容有可靠的信息来源支持。", + "provider": "alpha_engine" }, "fundamental_analysis": { "name": "基本面分析", - "model": "qwen-flash-2025-07-28", - "prompt_template": "# 角色\n你是一位专注于长期价值投资的顶级证券分析师,擅长从基本面出发,对公司进行深入、全面的分析。你的分析报告以客观、严谨、逻辑清晰、数据详实著称。\n# 任务\n为公司 {company_name} (股票代码: {ts_code}) 生成一份全面、专业、结构化的投资分析报告。\n# 输出要求\n直接开始:不要进行任何自我介绍或客套话,直接输出报告正文。\nMarkdown格式:使用清晰的多级Markdown标题(如 ## 和 ###)来组织报告结构。\n专业口吻:保持客观、中立、分析性的专业语调。\n信息缺失处理:如果某些信息在公开渠道无法获取,请明确指出“相关信息未公开披露”或类似说明。\n\n# 报告核心结构与分析要点\n一、 公司基本面分析 (Fundamental Analysis)\n1.1 护城河与核心竞争力\n公司通过何种独有优势(如品牌、技术、成本、网络效应、牌照等)获取超额利润?\n该护城河是在增强、维持还是在削弱?请提供论据。\n1.2 管理层与公司治理\n管理能力:管理层过往的战略决策和执行能力如何?是否有卓越的业界声誉?\n股东回报:管理层及大股东是否珍惜股权价值?(分析历史上的增持/减持行为、分红派息政策、是否存在损害小股东利益的体外资产等)\n激励与目标:公司的经营目标是长期主义还是短期化?管理层的激励机制(如股权激励、考核指标)是否与长期战略目标一致?\n1.3 企业文化与财务政策\n公司是否有独特且可观察到的企业文化?(例如:创新文化、成本控制文化等)\n公司的财务政策(如资本结构、现金流管理、投资策略)与同行业公司相比有何显著特点?是激进还是保守?\n1.4 发展历程与战略规划\n梳理公司发展史上的关键事件、重大业务转型或里程碑。\n公司是否有清晰的长期战略目标(未来5-10年)?计划成为一家什么样的企业?\n二、 业务与市场分析 (Business & Market Analysis)\n2.1 产品与客户价值\n公司为客户提供什么核心产品/服务?其核心价值点是什么?客户为何选择公司的产品而非竞争对手的?\n产品的更新迭代是颠覆性的还是渐进积累型的?分析产品历年的产量、价格及销量变化,并探讨其背后的驱动因素。\n2.2 市场需求与景气度\n客户所处行业的需求是趋势性的高增长,还是周期性波动?或是两者结合?当前处于何种阶段?\n目标客户群体的经营状况和现金流是否健康?\n2.3 议价能力与客户关系\n公司对下游客户的议价能力强弱如何?(结合应收账款周转天数、账龄结构、毛利率等数据进行佐证)\n公司与核心客户的关系是否稳定?客户对公司的评价如何(例如:客户忠诚度、满意度)?\n三、 竞争格局分析 (Competitive Landscape Analysis)\n3.1 竞争对手画像\n列出公司的主要竞争对手,并分析各自的优势与劣势。\n公司的竞争对手是在增多还是减少?行业进入壁垒是在增高还是降低?\n是否存在潜在的跨界竞争者?\n四、 供应链与外部关系 (Supply Chain & External Relations)\n4.1 供应链议价能力\n公司对上游供应商的议价能力如何?(结合应付账款周转天数、采购成本控制等数据进行佐证)\n核心供应商的经营是否稳定?供应链是否存在集中度过高的风险?\n4.2 金融机构关系与融资需求\n公司与金融机构的关系如何?融资渠道是否通畅?\n公司未来的发展是否依赖于大规模的债务或股权融资?\n五、 监管环境与政策风险 (Regulatory Environment & Policy Risks)\n公司所处行业是否存在重要的监管部门?主要的监管政策有哪些?\n监管政策是否稳定?未来可能发生哪些重大变化?对公司有何潜在影响?\n公司是否具备影响或适应监管政策变化的能力?" + "model": "deepseek-r1", + "prompt_template": "# 角色\n你是一位专注于长期价值投资的顶级证券分析师,擅长从基本面出发,对公司进行深入、全面的分析。你的分析报告以客观、严谨、逻辑清晰、数据详实著称。\n# 任务\n为公司 {company_name} (股票代码: {ts_code}) 生成一份全面、专业、结构化的投资分析报告。\n# 输出要求\n直接开始:不要进行任何自我介绍或客套话,直接输出报告正文。\nMarkdown格式:使用清晰的多级Markdown标题(如 ## 和 ###)来组织报告结构。\n专业口吻:保持客观、中立、分析性的专业语调。\n信息缺失处理:如果某些信息在公开渠道无法获取,请明确指出“相关信息未公开披露”或类似说明。\n\n# 报告核心结构与分析要点\n一、 公司基本面分析 (Fundamental Analysis)\n1.1 护城河与核心竞争力\n公司通过何种独有优势(如品牌、技术、成本、网络效应、牌照等)获取超额利润?\n该护城河是在增强、维持还是在削弱?请提供论据。\n1.2 管理层与公司治理\n管理能力:管理层过往的战略决策和执行能力如何?是否有卓越的业界声誉?\n股东回报:管理层及大股东是否珍惜股权价值?(分析历史上的增持/减持行为、分红派息政策、是否存在损害小股东利益的体外资产等)\n激励与目标:公司的经营目标是长期主义还是短期化?管理层的激励机制(如股权激励、考核指标)是否与长期战略目标一致?\n1.3 企业文化与财务政策\n公司是否有独特且可观察到的企业文化?(例如:创新文化、成本控制文化等)\n公司的财务政策(如资本结构、现金流管理、投资策略)与同行业公司相比有何显著特点?是激进还是保守?\n1.4 发展历程与战略规划\n梳理公司发展史上的关键事件、重大业务转型或里程碑。\n公司是否有清晰的长期战略目标(未来5-10年)?计划成为一家什么样的企业?\n二、 业务与市场分析 (Business & Market Analysis)\n2.1 产品与客户价值\n公司为客户提供什么核心产品/服务?其核心价值点是什么?客户为何选择公司的产品而非竞争对手的?\n产品的更新迭代是颠覆性的还是渐进积累型的?分析产品历年的产量、价格及销量变化,并探讨其背后的驱动因素。\n2.2 市场需求与景气度\n客户所处行业的需求是趋势性的高增长,还是周期性波动?或是两者结合?当前处于何种阶段?\n目标客户群体的经营状况和现金流是否健康?\n2.3 议价能力与客户关系\n公司对下游客户的议价能力强弱如何?(结合应收账款周转天数、账龄结构、毛利率等数据进行佐证)\n公司与核心客户的关系是否稳定?客户对公司的评价如何(例如:客户忠诚度、满意度)?\n三、 竞争格局分析 (Competitive Landscape Analysis)\n3.1 竞争对手画像\n列出公司的主要竞争对手,并分析各自的优势与劣势。\n公司的竞争对手是在增多还是减少?行业进入壁垒是在增高还是降低?\n是否存在潜在的跨界竞争者?\n四、 供应链与外部关系 (Supply Chain & External Relations)\n4.1 供应链议价能力\n公司对上游供应商的议价能力如何?(结合应付账款周转天数、采购成本控制等数据进行佐证)\n核心供应商的经营是否稳定?供应链是否存在集中度过高的风险?\n4.2 金融机构关系与融资需求\n公司与金融机构的关系如何?融资渠道是否通畅?\n公司未来的发展是否依赖于大规模的债务或股权融资?\n五、 监管环境与政策风险 (Regulatory Environment & Policy Risks)\n公司所处行业是否存在重要的监管部门?主要的监管政策有哪些?\n监管政策是否稳定?未来可能发生哪些重大变化?对公司有何潜在影响?\n公司是否具备影响或适应监管政策变化的能力?", + "provider": "alpha_engine" }, "bull_case": { "name": "看涨分析", - "model": "qwen-flash-2025-07-28", + "model": "deepseek-r1", "dependencies": [], - "prompt_template": "#### # 角色\n你是一位顶级的成长股投资分析师,拥有敏锐的洞察力,尤其擅长**挖掘市场尚未充分认识到的潜在价值**和**判断长期行业趋势**。你的任务是为目标公司构建一个令人信服的、由证据支持的看涨论述(Bull Case)。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份深入的看涨分析报告。报告的核心是论证该公司拥有被市场低估的隐藏资产、持续加深的护城河,并且其所处行业将迎来至少3年以上的景气周期。\n\n#### # 输出要求\n1. **直奔主题**:直接开始分析,无需引言。\n2. **Markdown格式**:使用清晰的标题结构来组织你的论点。\n3. **数据与来源**:所有关键论点都必须有数据、事实或合理的逻辑推演作为支撑。请用*斜体*注明信息来源(如:*来源:公司2023年投资者交流纪要* 或 *来源:中信证券行业研报*)。\n4. **聚焦看涨逻辑**:报告内容应完全围绕支撑看涨观点的论据展开,暂时忽略风险和负面因素。\n5. **前瞻性视角**:分析应侧重于未来3-5年的发展潜力,而不仅仅是回顾历史。\n6. **信息缺失处理**:如果某些推论需要的数据无法公开获取,可以基于现有信息进行合理的逻辑推测,并明确标注“(此为基于...的推测)”。\n\n---\n\n### # 看涨核心论证框架\n\n## 一、 深度挖掘:公司的隐藏资产与未被市场充分定价的价值\n\n### 1.1 资产负债表之外的价值 (Off-Balance Sheet Value)\n- **无形资产**:公司是否拥有未被充分计价的核心技术专利、软件著作权、特许经营权或强大的品牌价值?请量化或举例说明其潜在商业价值。\n- **数据资产**:公司是否积累了具有巨大潜在价值的用户或行业数据?这些数据未来可能的变现途径是什么?\n\n### 1.2 低估的实体或股权资产 (Undervalued Physical or Equity Assets)\n- **土地/物业重估**:公司持有的土地、房产等固定资产,其当前市场公允价值是否远超账面价值?\n- **子公司/投资价值**:公司旗下是否有快速增长但未被市场充分关注的子公司或有价值的长期股权投资?分析其独立估值的潜力。\n\n### 1.3 运营中的“隐形冠军” (Operational \"Hidden Champions\")\n- 公司是否存在独特的、难以复制的生产工艺、供应链管理能力或运营效率优势,而这些优势尚未完全体现在当前的利润率中?\n\n## 二、 护城河的加深:竞争优势的动态强化分析\n\n### 2.1 护城河的动态演变:是静态还是在拓宽?\n- 论证公司的核心护城河(例如:网络效应、转换成本、成本优势、技术壁垒)在未来几年将如何被**强化**而非削弱。请提供具体证据(如:研发投入的持续增长、客户续约率的提升、市场份额的扩大等)。\n\n### 2.2 技术与创新壁垒的领先优势\n- 公司的研发投入和创新产出,如何确保其在未来3-5年内保持对竞争对手的技术代差或领先地位?\n- 是否有即将商业化的“杀手级”新产品或新技术?\n\n### 2.3 品牌与客户粘性的正反馈循环\n- 公司的品牌价值或客户关系如何形成一个正反馈循环(即:强品牌带来高议价能力 -> 高利润投入研发/营销 -> 品牌更强)?\n- 客户为何难以转向竞争对手?分析其高昂的转换成本。\n\n## 三、 长期景气度:行业未来3年以上的持续增长动力\n\n### 3.1 长期需求驱动力(Demand-Side Drivers)\n- 驱动行业增长的核心动力是短期的周期性复苏,还是长期的结构性变迁(如:技术革命、消费升级、国产替代、政策驱动)?请深入论证。\n- 行业的市场渗透率是否仍有巨大提升空间?分析未来市场规模(TAM)的扩张潜力。\n\n### 3.2 供给侧格局优化(Supply-Side Dynamics)\n- 行业供给侧是否出现集中度提升、落后产能出清的趋势?这是否意味着龙头企业的定价权和盈利能力将持续增强?\n- 行业的进入壁垒是否在显著提高(如:技术、资金、资质壁垒),从而限制新竞争者的涌入?\n\n### 3.3 关键催化剂(Key Catalysts)\n- 未来1-2年内,是否存在可以显著提升公司估值或盈利的潜在催化剂事件(如:新产品发布、重要政策落地、海外市场突破等)?" + "prompt_template": "#### # 角色\n你是一位顶级的成长股投资分析师,拥有敏锐的洞察力,尤其擅长**挖掘市场尚未充分认识到的潜在价值**和**判断长期行业趋势**。你的任务是为目标公司构建一个令人信服的、由证据支持的看涨论述(Bull Case)。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份深入的看涨分析报告。报告的核心是论证该公司拥有被市场低估的隐藏资产、持续加深的护城河,并且其所处行业将迎来至少3年以上的景气周期。\n\n#### # 输出要求\n1. **直奔主题**:直接开始分析,无需引言。\n2. **Markdown格式**:使用清晰的标题结构来组织你的论点。\n3. **数据与来源**:所有关键论点都必须有数据、事实或合理的逻辑推演作为支撑。请用*斜体*注明信息来源(如:*来源:公司2023年投资者交流纪要* 或 *来源:中信证券行业研报*)。\n4. **聚焦看涨逻辑**:报告内容应完全围绕支撑看涨观点的论据展开,暂时忽略风险和负面因素。\n5. **前瞻性视角**:分析应侧重于未来3-5年的发展潜力,而不仅仅是回顾历史。\n6. **信息缺失处理**:如果某些推论需要的数据无法公开获取,可以基于现有信息进行合理的逻辑推测,并明确标注“(此为基于...的推测)”。\n\n---\n\n### # 看涨核心论证框架\n\n## 一、 深度挖掘:公司的隐藏资产与未被市场充分定价的价值\n\n### 1.1 资产负债表之外的价值 (Off-Balance Sheet Value)\n- **无形资产**:公司是否拥有未被充分计价的核心技术专利、软件著作权、特许经营权或强大的品牌价值?请量化或举例说明其潜在商业价值。\n- **数据资产**:公司是否积累了具有巨大潜在价值的用户或行业数据?这些数据未来可能的变现途径是什么?\n\n### 1.2 低估的实体或股权资产 (Undervalued Physical or Equity Assets)\n- **土地/物业重估**:公司持有的土地、房产等固定资产,其当前市场公允价值是否远超账面价值?\n- **子公司/投资价值**:公司旗下是否有快速增长但未被市场充分关注的子公司或有价值的长期股权投资?分析其独立估值的潜力。\n\n### 1.3 运营中的“隐形冠军” (Operational \"Hidden Champions\")\n- 公司是否存在独特的、难以复制的生产工艺、供应链管理能力或运营效率优势,而这些优势尚未完全体现在当前的利润率中?\n\n## 二、 护城河的加深:竞争优势的动态强化分析\n\n### 2.1 护城河的动态演变:是静态还是在拓宽?\n- 论证公司的核心护城河(例如:网络效应、转换成本、成本优势、技术壁垒)在未来几年将如何被**强化**而非削弱。请提供具体证据(如:研发投入的持续增长、客户续约率的提升、市场份额的扩大等)。\n\n### 2.2 技术与创新壁垒的领先优势\n- 公司的研发投入和创新产出,如何确保其在未来3-5年内保持对竞争对手的技术代差或领先地位?\n- 是否有即将商业化的“杀手级”新产品或新技术?\n\n### 2.3 品牌与客户粘性的正反馈循环\n- 公司的品牌价值或客户关系如何形成一个正反馈循环(即:强品牌带来高议价能力 -> 高利润投入研发/营销 -> 品牌更强)?\n- 客户为何难以转向竞争对手?分析其高昂的转换成本。\n\n## 三、 长期景气度:行业未来3年以上的持续增长动力\n\n### 3.1 长期需求驱动力(Demand-Side Drivers)\n- 驱动行业增长的核心动力是短期的周期性复苏,还是长期的结构性变迁(如:技术革命、消费升级、国产替代、政策驱动)?请深入论证。\n- 行业的市场渗透率是否仍有巨大提升空间?分析未来市场规模(TAM)的扩张潜力。\n\n### 3.2 供给侧格局优化(Supply-Side Dynamics)\n- 行业供给侧是否出现集中度提升、落后产能出清的趋势?这是否意味着龙头企业的定价权和盈利能力将持续增强?\n- 行业的进入壁垒是否在显著提高(如:技术、资金、资质壁垒),从而限制新竞争者的涌入?\n\n### 3.3 关键催化剂(Key Catalysts)\n- 未来1-2年内,是否存在可以显著提升公司估值或盈利的潜在催化剂事件(如:新产品发布、重要政策落地、海外市场突破等)?", + "provider": "alpha_engine" }, "bear_case": { "name": "看跌分析", - "model": "qwen-flash-2025-07-28", + "model": "deepseek-r1", "dependencies": [], - "prompt_template": "#### # 角色\n你是一位经验丰富的风险控制分析师和审慎的价值投资者,以“能看到别人看不到的风险”而闻名。你的核心任务是**进行压力测试**,识别出公司潜在的、可能导致价值毁灭的重大风险点,并评估其在最坏情况下的价值底线。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份审慎的看跌分析报告(Bear Case)。报告需要深入探讨可能侵蚀公司护城河的因素、被市场忽视的潜在风险、行业可能面临的逆风,并对公司的价值底线进行评估。\n\n#### # 输出要求\n1. **直奔主题**:直接开始风险分析,无需引言。\n2. **Markdown格式**:使用清晰的标题结构组织风险论点。\n3. **证据驱动**:所有风险点都必须基于事实、数据或严谨的逻辑推演。请用*斜体*注明信息来源(如:*来源:竞争对手2023年财报* 或 *来源:行业监管政策草案*)。\n4. **聚焦看跌逻辑**:报告应完全围绕看跌观点展开,旨在识别和放大潜在的负面因素。\n5. **底线思维**:分析的核心是评估“事情最坏能到什么程度”,并判断公司的安全边际。\n6. **信息缺失处理**:对于难以量化的风险(如管理层风险),进行定性分析和逻辑阐述。\n\n---\n\n### # 看跌核心论证框架\n\n## 一、 护城河的侵蚀:竞争优势的脆弱性分析 (Moat Erosion: Vulnerability of Competitive Advantages)\n\n### 1.1 现有护城河的潜在威胁\n- 公司的核心护城河(技术、品牌、成本等)是否面临被颠覆的风险?(例如:新技术的出现、竞争对手的模仿或价格战)\n- 客户的转换成本是否真的足够高?是否存在某些因素(如行业标准化)可能降低客户的转换壁垒?\n\n### 1.2 竞争格局的恶化\n- 是否有新的、强大的“跨界”竞争者进入市场?\n- 行业是否从“蓝海”变为“红海”?分析导致竞争加剧的因素(如:产能过剩、产品同质化)。\n- 竞争对手的哪些战略举动可能对公司构成致命打击?\n\n## 二、 隐藏的负债与风险:资产负债表之外的“地雷” (Hidden Liabilities & Risks: Off-Balance Sheet \"Mines\")\n\n### 2.1 潜在的财务风险\n- 公司是否存在大量的或有负债、对外担保或未入表的债务?\n- 公司的现金流健康状况是否脆弱?分析其经营现金流能否覆盖资本开支和债务利息,尤其是在收入下滑的情况下。\n- 应收账款或存货是否存在潜在的暴雷风险?(分析其账龄、周转率和减值计提的充分性)\n\n### 2.2 运营与管理风险\n- 公司是否对单一供应商、单一客户或单一市场存在过度依赖?\n- 公司是否存在“关键人物风险”?创始团队或核心技术人员的离开会对公司造成多大影响?\n- 公司的企业文化或治理结构是否存在可能导致重大决策失误的缺陷?\n\n## 三、 行业逆风与最坏情况分析 (Industry Headwinds & Worst-Case Scenario)\n\n### 3.1 行业天花板与需求逆转\n- 行业渗透率是否已接近饱和?未来的增长空间是否被高估?\n- 驱动行业增长的核心因素是否可持续?是否存在可能导致需求突然逆转的黑天鹅事件(如:政策突变、技术路线改变、消费者偏好转移)?\n\n### 3.2 价值链上的压力传导\n- 上游供应商的议价能力是否在增强,从而挤压公司利润空间?\n- 下游客户的需求是否在萎缩,或者客户的财务状况是否在恶化?\n\n### 3.3 最坏情况压力测试 (Worst-Case Stress Test)\n- **情景假设**:假设行业需求下滑30%,或主要竞争对手发起价格战,公司的收入、利润和现金流会受到多大冲击?\n- **破产风险评估**:在这种极端情况下,公司是否有足够的现金储备和融资能力来度过危机?公司的生存底线在哪里?\n\n### 3.4 价值底线评估:清算价值分析 (Bottom-Line Valuation: Liquidation Value Analysis)\n- **核心假设**:在公司被迫停止经营并清算的极端情况下,其资产的真实变现价值是多少?\n- **资产逐项折价**:请对资产负债表中的主要科目进行折价估算。例如:\n - *现金及等价物*:按100%计算。\n - *应收账款*:根据账龄和客户质量,估计一个合理的回收率(如50%-80%)。\n - *存货*:根据存货类型(原材料、产成品)和市场状况,估计一个变现折扣(如30%-70%)。\n - *固定资产(厂房、设备)*:估计其二手市场的变现价值,通常远低于账面净值。\n - *无形资产/商誉*:大部分在清算时价值归零。\n- **负债计算**:公司的总负债(包括所有表内及表外负债)需要被优先偿还。\n- **清算价值估算**:计算**(折价后的总资产 - 总负债)/ 总股本**,得出每股清算价值。这是公司价值的绝对底线。\n\n## 四、 估值陷阱分析 (Valuation Trap Analysis)\n\n### 4.1 增长预期的证伪\n- 当前的高估值是否隐含了过于乐观的增长预期?论证这些预期为何可能无法实现。\n- 市场是否忽略了公司盈利能力的周期性,而将其误判为长期成长性?\n\n### 4.2 资产质量重估\n- 公司的资产(尤其是商誉、无形资产)是否存在大幅减值的风险?\n- 公司的真实盈利能力(扣除非经常性损益后)是否低于报表利润?\n" + "prompt_template": "#### # 角色\n你是一位经验丰富的风险控制分析师和审慎的价值投资者,以“能看到别人看不到的风险”而闻名。你的核心任务是**进行压力测试**,识别出公司潜在的、可能导致价值毁灭的重大风险点,并评估其在最坏情况下的价值底线。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份审慎的看跌分析报告(Bear Case)。报告需要深入探讨可能侵蚀公司护城河的因素、被市场忽视的潜在风险、行业可能面临的逆风,并对公司的价值底线进行评估。\n\n#### # 输出要求\n1. **直奔主题**:直接开始风险分析,无需引言。\n2. **Markdown格式**:使用清晰的标题结构组织风险论点。\n3. **证据驱动**:所有风险点都必须基于事实、数据或严谨的逻辑推演。请用*斜体*注明信息来源(如:*来源:竞争对手2023年财报* 或 *来源:行业监管政策草案*)。\n4. **聚焦看跌逻辑**:报告应完全围绕看跌观点展开,旨在识别和放大潜在的负面因素。\n5. **底线思维**:分析的核心是评估“事情最坏能到什么程度”,并判断公司的安全边际。\n6. **信息缺失处理**:对于难以量化的风险(如管理层风险),进行定性分析和逻辑阐述。\n\n---\n\n### # 看跌核心论证框架\n\n## 一、 护城河的侵蚀:竞争优势的脆弱性分析 (Moat Erosion: Vulnerability of Competitive Advantages)\n\n### 1.1 现有护城河的潜在威胁\n- 公司的核心护城河(技术、品牌、成本等)是否面临被颠覆的风险?(例如:新技术的出现、竞争对手的模仿或价格战)\n- 客户的转换成本是否真的足够高?是否存在某些因素(如行业标准化)可能降低客户的转换壁垒?\n\n### 1.2 竞争格局的恶化\n- 是否有新的、强大的“跨界”竞争者进入市场?\n- 行业是否从“蓝海”变为“红海”?分析导致竞争加剧的因素(如:产能过剩、产品同质化)。\n- 竞争对手的哪些战略举动可能对公司构成致命打击?\n\n## 二、 隐藏的负债与风险:资产负债表之外的“地雷” (Hidden Liabilities & Risks: Off-Balance Sheet \"Mines\")\n\n### 2.1 潜在的财务风险\n- 公司是否存在大量的或有负债、对外担保或未入表的债务?\n- 公司的现金流健康状况是否脆弱?分析其经营现金流能否覆盖资本开支和债务利息,尤其是在收入下滑的情况下。\n- 应收账款或存货是否存在潜在的暴雷风险?(分析其账龄、周转率和减值计提的充分性)\n\n### 2.2 运营与管理风险\n- 公司是否对单一供应商、单一客户或单一市场存在过度依赖?\n- 公司是否存在“关键人物风险”?创始团队或核心技术人员的离开会对公司造成多大影响?\n- 公司的企业文化或治理结构是否存在可能导致重大决策失误的缺陷?\n\n## 三、 行业逆风与最坏情况分析 (Industry Headwinds & Worst-Case Scenario)\n\n### 3.1 行业天花板与需求逆转\n- 行业渗透率是否已接近饱和?未来的增长空间是否被高估?\n- 驱动行业增长的核心因素是否可持续?是否存在可能导致需求突然逆转的黑天鹅事件(如:政策突变、技术路线改变、消费者偏好转移)?\n\n### 3.2 价值链上的压力传导\n- 上游供应商的议价能力是否在增强,从而挤压公司利润空间?\n- 下游客户的需求是否在萎缩,或者客户的财务状况是否在恶化?\n\n### 3.3 最坏情况压力测试 (Worst-Case Stress Test)\n- **情景假设**:假设行业需求下滑30%,或主要竞争对手发起价格战,公司的收入、利润和现金流会受到多大冲击?\n- **破产风险评估**:在这种极端情况下,公司是否有足够的现金储备和融资能力来度过危机?公司的生存底线在哪里?\n\n### 3.4 价值底线评估:清算价值分析 (Bottom-Line Valuation: Liquidation Value Analysis)\n- **核心假设**:在公司被迫停止经营并清算的极端情况下,其资产的真实变现价值是多少?\n- **资产逐项折价**:请对资产负债表中的主要科目进行折价估算。例如:\n - *现金及等价物*:按100%计算。\n - *应收账款*:根据账龄和客户质量,估计一个合理的回收率(如50%-80%)。\n - *存货*:根据存货类型(原材料、产成品)和市场状况,估计一个变现折扣(如30%-70%)。\n - *固定资产(厂房、设备)*:估计其二手市场的变现价值,通常远低于账面净值。\n - *无形资产/商誉*:大部分在清算时价值归零。\n- **负债计算**:公司的总负债(包括所有表内及表外负债)需要被优先偿还。\n- **清算价值估算**:计算**(折价后的总资产 - 总负债)/ 总股本**,得出每股清算价值。这是公司价值的绝对底线。\n\n## 四、 估值陷阱分析 (Valuation Trap Analysis)\n\n### 4.1 增长预期的证伪\n- 当前的高估值是否隐含了过于乐观的增长预期?论证这些预期为何可能无法实现。\n- 市场是否忽略了公司盈利能力的周期性,而将其误判为长期成长性?\n\n### 4.2 资产质量重估\n- 公司的资产(尤其是商誉、无形资产)是否存在大幅减值的风险?\n- 公司的真实盈利能力(扣除非经常性损益后)是否低于报表利润?\n", + "provider": "alpha_engine" }, "market_analysis": { "name": "市场分析", - "model": "qwen-flash", - "prompt_template": "#### # 角色\n你是一位顶级的市场策略分析师,精通行为金融学,对市场情绪和投资者心理有深刻的洞察。你擅长从海量的新闻、研报和市场数据中,提炼出当前市场对特定公司的核心看法、主要分歧点,并预判可能导致情绪反转的关键驱动因素。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份当前的市场情绪分析报告。报告应聚焦于解读市场参与者当下的想法,而不是对公司基本面进行独立研究。\n\n#### # 输出要求\n1. **基于近期信息**:分析必须基**最近1-3个月**的公开新闻、分析师评论、社交媒体讨论和市场数据。\n2. **引用新闻来源**:在提到具体事件或观点时,必须用*斜体*注明新闻或信息来源。\n3. **客观呈现分歧**:清晰、中立地展示市场上多空双方的观点,而不是偏向任何一方。\n4. **聚焦“预期差”**:分析的核心是找出市场预期与公司现实之间可能存在的差距。\n5. **Markdown格式**:使用清晰的标题结构组织报告。\n\n---\n\n### # 市场情绪分析框架\n\n## 一、 当前市场主流叙事与估值定位 (Current Market Narrative & Valuation Positioning)\n\n### 1.1 市场的主流故事线是什么?\n- 综合近期(1-3个月内)的新闻报道和券商研报,当前市场在为这家公司讲述一个什么样的“故事”?是“困境反转”、“AI赋能”、“周期复苏”还是“增长放缓”?\n- 这个主流故事线是在近期被强化了,还是开始出现动摇?\n\n### 1.2 当前估值反映了什么预期?\n- 公司当前的估值水平(如市盈率P/E、市净率P/B)在历史和行业中处于什么位置(高位、中位、低位)?\n- 这个估值水平背后,市场“计价”了什么样的增长率、利润率或成功预期?*(例如:市场普遍预期其新业务明年将贡献30%的收入增长)*\n\n## 二、 情绪分歧点:多空双方的核心博弈 (Points of Disagreement: The Core Bull vs. Bear Debate)\n\n### 2.1 关键分歧一:[例如:新产品的市场前景]\n- **看多者认为**:[陈述看多方的核心理由,并引用支持性新闻或数据]\n- **看空者认为**:[陈述看空方的核心理由,并引用支持性新闻或数据]\n\n### 2.2 关键分歧二:[例如:监管政策的影响]\n- **看多者认为**:[陈述看多方的核心理由,并引用支持性新闻或数据]\n- **看空者认为**:[陈述看空方的核心理由,并引用支持性新闻或数据]\n\n### 2.3 市场资金的态度\n- 近期是否有知名的机构投资者在增持或减持?\n- 股票的卖空比例是否有显著变化?这反映了什么情绪?\n\n## 三、 情绪变化的潜在驱动力 (Potential Drivers of Sentiment Change)\n\n### 3.1 近期(未来1-3个月)的关键催化剂\n- 列出未来短期内可能打破当前市场情绪平衡的关键事件。(例如:即将发布的财报、行业重要会议、新产品发布会、重要的宏观数据公布等)\n- 这些事件的结果将如何分别验证或证伪当前多/空双方的逻辑?\n\n### 3.2 识别“预期差”\n- 当前市场最可能“过度乐观”的点是什么?\n- 当前市场最可能“过度悲观”的点是什么?\n- 未来什么样的信息出现,会最大程度地修复这种预期差,并引发股价剧烈波动?\n" + "model": "deepseek-r1", + "prompt_template": "#### # 角色\n你是一位顶级的市场策略分析师,精通行为金融学,对市场情绪和投资者心理有深刻的洞察。你擅长从海量的新闻、研报和市场数据中,提炼出当前市场对特定公司的核心看法、主要分歧点,并预判可能导致情绪反转的关键驱动因素。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份当前的市场情绪分析报告。报告应聚焦于解读市场参与者当下的想法,而不是对公司基本面进行独立研究。\n\n#### # 输出要求\n1. **基于近期信息**:分析必须基**最近1-3个月**的公开新闻、分析师评论、社交媒体讨论和市场数据。\n2. **引用新闻来源**:在提到具体事件或观点时,必须用*斜体*注明新闻或信息来源。\n3. **客观呈现分歧**:清晰、中立地展示市场上多空双方的观点,而不是偏向任何一方。\n4. **聚焦“预期差”**:分析的核心是找出市场预期与公司现实之间可能存在的差距。\n5. **Markdown格式**:使用清晰的标题结构组织报告。\n\n---\n\n### # 市场情绪分析框架\n\n## 一、 当前市场主流叙事与估值定位 (Current Market Narrative & Valuation Positioning)\n\n### 1.1 市场的主流故事线是什么?\n- 综合近期(1-3个月内)的新闻报道和券商研报,当前市场在为这家公司讲述一个什么样的“故事”?是“困境反转”、“AI赋能”、“周期复苏”还是“增长放缓”?\n- 这个主流故事线是在近期被强化了,还是开始出现动摇?\n\n### 1.2 当前估值反映了什么预期?\n- 公司当前的估值水平(如市盈率P/E、市净率P/B)在历史和行业中处于什么位置(高位、中位、低位)?\n- 这个估值水平背后,市场“计价”了什么样的增长率、利润率或成功预期?*(例如:市场普遍预期其新业务明年将贡献30%的收入增长)*\n\n## 二、 情绪分歧点:多空双方的核心博弈 (Points of Disagreement: The Core Bull vs. Bear Debate)\n\n### 2.1 关键分歧一:[例如:新产品的市场前景]\n- **看多者认为**:[陈述看多方的核心理由,并引用支持性新闻或数据]\n- **看空者认为**:[陈述看空方的核心理由,并引用支持性新闻或数据]\n\n### 2.2 关键分歧二:[例如:监管政策的影响]\n- **看多者认为**:[陈述看多方的核心理由,并引用支持性新闻或数据]\n- **看空者认为**:[陈述看空方的核心理由,并引用支持性新闻或数据]\n\n### 2.3 市场资金的态度\n- 近期是否有知名的机构投资者在增持或减持?\n- 股票的卖空比例是否有显著变化?这反映了什么情绪?\n\n## 三、 情绪变化的潜在驱动力 (Potential Drivers of Sentiment Change)\n\n### 3.1 近期(未来1-3个月)的关键催化剂\n- 列出未来短期内可能打破当前市场情绪平衡的关键事件。(例如:即将发布的财报、行业重要会议、新产品发布会、重要的宏观数据公布等)\n- 这些事件的结果将如何分别验证或证伪当前多/空双方的逻辑?\n\n### 3.2 识别“预期差”\n- 当前市场最可能“过度乐观”的点是什么?\n- 当前市场最可能“过度悲观”的点是什么?\n- 未来什么样的信息出现,会最大程度地修复这种预期差,并引发股价剧烈波动?\n", + "provider": "alpha_engine" }, "news_analysis": { "name": "新闻分析", - "model": "qwen-flash-2025-07-28", - "prompt_template": "#### # 角色\n你是一位嗅觉极其敏锐的金融新闻分析师,专注于事件驱动投资策略。你擅长从看似孤立的新闻事件中,解读其深层含义,并精准预判其对公司股价可能造成的催化作用和潜在的拐点。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份股价催化剂与拐点预判报告。报告需要梳理近期相关新闻,并基于这些信息,识别出未来可能导致股价发生重大变化的正面及负面催化剂。\n\n#### # 输出要求\n1. **聚焦近期新闻**:分析应主要基于**最近1-2个月**的公司公告、行业新闻、政策文件及权威媒体报道。\n2. **明确时间线**:尽可能为潜在的催化剂事件标注一个预期的时间窗口(例如:“预计在Q4财报发布时”、“未来一个月内”)。\n3. **量化影响**:对于每个催化剂,不仅要定性判断(利好/利空),还要尝试分析其可能的影响级别(重大/中等/轻微)。\n4. **提供观察信号**:为每个预判的拐点,提供需要密切观察的关键信号或数据验证点。\n5. **Markdown格式**:使用清晰的标题结构。\n6. **引用来源**:关键信息需用*斜体*注明来源。\n\n---\n\n### # 股价催化剂与拐点分析框架\n\n## 一、 近期关键新闻梳理与解读 (Recent Key News Flow & Interpretation)\n\n- **新闻事件1:[日期] [新闻标题]**\n - *来源:[例如:公司官网公告 / 彭博社]*\n - **事件概述**:[简要概括新闻内容]\n - **市场初步反应**:[事件发生后,股价和成交量有何变化?]\n - **深层解读**:[该新闻是孤立事件,还是某个趋势的延续?它暗示了公司基本面的何种变化?]\n- **新闻事件2:[日期] [新闻标题]**\n - ... (以此类推)\n\n## 二、 正面催化剂预判 (Potential Positive Catalysts)\n\n### 2.1 确定性较高的催化剂 (High-Probability Catalysts)\n- **催化剂名称**:[例如:新一代产品发布]\n- **预期时间窗口**:[例如:预计在下个月的行业大会上]\n- **触发逻辑**:[为什么这件事会成为股价的正面驱动力?它会如何改善市场预期?]\n- **需观察的信号**:[需要看到什么具体信息(如产品性能参数、预订单数量)才能确认催化剂的有效性?]\n\n### 2.2 潜在的“黑天鹅”式利好 (Potential \"Black Swan\" Positives)\n- **催化剂名称**:[例如:意外获得海外市场准入 / 竞争对手出现重大失误]\n- **触发逻辑**:[描述这种小概率但影响巨大的利好事件及其可能性]\n- **需观察的信号**:[哪些先行指标或行业动态可能预示着这种事件的发生?]\n\n## 三、 负面催化剂预判 (Potential Negative Catalysts)\n\n### 3.1 确定性较高的风险 (High-Probability Risks)\n- **催化剂名称**:[例如:关键专利到期 / 主要客户合同续约谈判]\n- **预期时间窗口**:[例如:本季度末]\n- **触发逻辑**:[为什么这件事可能对股价造成负面冲击?]\n- **需观察的信号**:[需要关注哪些数据或公告来判断风险是否会兑现?]\n\n### 3.2 潜在的“黑天鹅”式风险 (Potential \"Black Swan\" Risks)\n- **催化剂名称**:[例如:突发性的行业监管收紧 / 供应链“断链”风险]\n- **触发逻辑**:[描述这种小概率但影响巨大的风险事件]\n- **需观察的信号**:[哪些蛛丝马迹可能预示着风险的临近?]\n\n## 四、 综合预判:下一个股价拐点 (Synthesis: The Next Inflection Point)\n\n- **核心博弈点**:综合以上分析,当前市场最关注、最可能率先发生的多空催化剂是什么?\n- **拐点预测**:基于当前信息,下一个可能改变股价趋势的关键时间点或事件最有可能是什么?\n- **关键验证指标**:在那个拐点到来之前,我们应该把注意力集中在哪个/哪些最关键的数据或信息上?\n" + "model": "deepseek-r1", + "prompt_template": "#### # 角色\n你是一位嗅觉极其敏锐的金融新闻分析师,专注于事件驱动投资策略。你擅长从看似孤立的新闻事件中,解读其深层含义,并精准预判其对公司股价可能造成的催化作用和潜在的拐点。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份股价催化剂与拐点预判报告。报告需要梳理近期相关新闻,并基于这些信息,识别出未来可能导致股价发生重大变化的正面及负面催化剂。\n\n#### # 输出要求\n1. **聚焦近期新闻**:分析应主要基于**最近1-2个月**的公司公告、行业新闻、政策文件及权威媒体报道。\n2. **明确时间线**:尽可能为潜在的催化剂事件标注一个预期的时间窗口(例如:“预计在Q4财报发布时”、“未来一个月内”)。\n3. **量化影响**:对于每个催化剂,不仅要定性判断(利好/利空),还要尝试分析其可能的影响级别(重大/中等/轻微)。\n4. **提供观察信号**:为每个预判的拐点,提供需要密切观察的关键信号或数据验证点。\n5. **Markdown格式**:使用清晰的标题结构。\n6. **引用来源**:关键信息需用*斜体*注明来源。\n\n---\n\n### # 股价催化剂与拐点分析框架\n\n## 一、 近期关键新闻梳理与解读 (Recent Key News Flow & Interpretation)\n\n- **新闻事件1:[日期] [新闻标题]**\n - *来源:[例如:公司官网公告 / 彭博社]*\n - **事件概述**:[简要概括新闻内容]\n - **市场初步反应**:[事件发生后,股价和成交量有何变化?]\n - **深层解读**:[该新闻是孤立事件,还是某个趋势的延续?它暗示了公司基本面的何种变化?]\n- **新闻事件2:[日期] [新闻标题]**\n - ... (以此类推)\n\n## 二、 正面催化剂预判 (Potential Positive Catalysts)\n\n### 2.1 确定性较高的催化剂 (High-Probability Catalysts)\n- **催化剂名称**:[例如:新一代产品发布]\n- **预期时间窗口**:[例如:预计在下个月的行业大会上]\n- **触发逻辑**:[为什么这件事会成为股价的正面驱动力?它会如何改善市场预期?]\n- **需观察的信号**:[需要看到什么具体信息(如产品性能参数、预订单数量)才能确认催化剂的有效性?]\n\n### 2.2 潜在的“黑天鹅”式利好 (Potential \"Black Swan\" Positives)\n- **催化剂名称**:[例如:意外获得海外市场准入 / 竞争对手出现重大失误]\n- **触发逻辑**:[描述这种小概率但影响巨大的利好事件及其可能性]\n- **需观察的信号**:[哪些先行指标或行业动态可能预示着这种事件的发生?]\n\n## 三、 负面催化剂预判 (Potential Negative Catalysts)\n\n### 3.1 确定性较高的风险 (High-Probability Risks)\n- **催化剂名称**:[例如:关键专利到期 / 主要客户合同续约谈判]\n- **预期时间窗口**:[例如:本季度末]\n- **触发逻辑**:[为什么这件事可能对股价造成负面冲击?]\n- **需观察的信号**:[需要关注哪些数据或公告来判断风险是否会兑现?]\n\n### 3.2 潜在的“黑天鹅”式风险 (Potential \"Black Swan\" Risks)\n- **催化剂名称**:[例如:突发性的行业监管收紧 / 供应链“断链”风险]\n- **触发逻辑**:[描述这种小概率但影响巨大的风险事件]\n- **需观察的信号**:[哪些蛛丝马迹可能预示着风险的临近?]\n\n## 四、 综合预判:下一个股价拐点 (Synthesis: The Next Inflection Point)\n\n- **核心博弈点**:综合以上分析,当前市场最关注、最可能率先发生的多空催化剂是什么?\n- **拐点预测**:基于当前信息,下一个可能改变股价趋势的关键时间点或事件最有可能是什么?\n- **关键验证指标**:在那个拐点到来之前,我们应该把注意力集中在哪个/哪些最关键的数据或信息上?\n", + "provider": "alpha_engine" }, "trading_analysis": { "name": "交易分析", - "model": "qwen-flash-2025-07-28", - "prompt_template": "#### # 角色\n你是一位经验丰富的专业交易员,擅长将技术分析、市场赔率计算与基本面催化剂结合起来,制定高胜率的交易策略。你的决策核心是评估“风险回报比”,并寻找“基本面和资金面”可能形成共振(双击)的交易机会。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份可执行的交易分析报告。报告需要深入分析当前股价走势,评估潜在的上涨空间与风险,并判断其是否具备形成“戴维斯双击”式上涨的潜力。\n\n#### # 输出要求\n1. **图表导向**:分析应基于对价格图表(K线)、成交量和关键技术指标(如均线、MACD、RSI)的解读。\n2. **量化赔率**:明确计算并展示风险回报比(赔率),作为是否值得参与交易的核心依据。\n3. **明确信号**:给出清晰、无歧义的入场、止损和止盈信号。\n4. **客观中立**:只基于当前的市场数据和图表信号进行分析,避免主观臆测。\n5. **Markdown格式**:使用清晰的标题结构。\n\n---\n\n### # 交易策略分析框架\n\n## 一、 当前价格走势与结构分析 (Current Price Action & Structure Analysis)\n\n### 1.1 趋势与动能\n- **当前趋势**:股价目前处于明确的上升、下降还是盘整趋势中?(*参考:关键均线系统,如MA20, MA60, MA120的排列状态*)\n- **关键水平**:当前最重要的支撑位和阻力位分别在哪里?这些是历史高低点、均线位置还是成交密集区?\n- **量价关系**:近期的成交量与价格波动是否匹配?是否存在“价升量增”的健康上涨或“价跌量增”的恐慌抛售?\n\n### 1.2 图表形态\n- 近期是否形成了关键的K线形态?(例如:突破性阳线、反转信号)\n- 是否存在经典的图表形态?(例如:头肩底、W底、收敛三角形、箱体震荡)\n\n## 二、 市场体量与赔率计算 (Market Capacity & Risk/Reward Calculation)\n\n### 2.1 上涨空间评估 (Upside Potential)\n- 如果向上突破关键阻力位,下一个或几个现实的**目标价位**在哪里?(*参考:前期高点、斐波那契扩展位、形态测量目标*)\n- **潜在回报率**:从当前价格到主要目标价位的潜在上涨百分比是多少?\n\n### 2.2 风险评估与止损设置 (Downside Risk & Stop-Loss)\n- 如果交易逻辑被证伪,一个清晰、有效的**止损价位**应该设在哪里?(*参考:关键支撑位下方、上升趋势线下方*)\n- **潜在风险率**:从当前价格到止损价位的潜在下跌百分比是多少?\n\n### 2.3 赔率分析 (Risk/Reward Ratio)\n- 计算**风险回报比**(= 潜在回报率 / 潜在风险率)。这个比率是否具有吸引力?(*专业交易者通常要求至少大于 2:1 或 3:1*)\n- **市场体量**:该股的日均成交额是否足够大,能够容纳计划中的资金进出而不会造成显著的冲击成本?\n\n## 三、 增长路径:“双击”可能性评估 (Growth Path: \"Dual-Click\" Potential)\n\n### 3.1 基本面驱动力 (Fundamental Momentum)\n- 近期是否有或将要有**基本面催化剂**来支撑股价上涨?(*参考《股价催化剂分析》的结论,如:超预期的财报、新产品成功、行业政策利好*)\n- 这个基本面利好是能提供“一次性”的脉冲,还是能开启一个“持续性”的盈利增长周期?\n\n### 3.2 资金面驱动力 (Capital Momentum)\n- 是否有证据表明**增量资金**正在流入?(*参考:成交量的持续放大、机构投资者的增持报告、龙虎榜数据*)\n- 该股所属的板块或赛道,当前是否受到市场主流资金的青睐?\n\n### 3.3 “双击”可能性综合评估\n- 综合来看,公司出现“**业绩超预期(基本面)+ 估值提升(资金面)**”双击局面的可能性有多大?\n- 触发“双击”的关键信号可能是什么?(例如:在发布亮眼财报后,股价以放量涨停的方式突破关键阻力位)\n\n## 四、 交易计划总结 (Actionable Trading Plan)\n\n- **入场信号**:[具体的入场条件。例如:日线收盘价站上 {阻力位A} 并且成交量放大至 {数值X} 以上]\n- **止损策略**:[具体的止损条件。例如:日线收盘价跌破 {支撑位B}]\n- **止盈策略**:[具体的目标位和操作。例如:在 {目标位C} 止盈50%,剩余仓位跟踪止盈]\n- **仓位管理**:[基于赔率和确定性,建议的初始仓位是多少?]\n" + "model": "deepseek-r1", + "prompt_template": "#### # 角色\n你是一位经验丰富的专业交易员,擅长将技术分析、市场赔率计算与基本面催化剂结合起来,制定高胜率的交易策略。你的决策核心是评估“风险回报比”,并寻找“基本面和资金面”可能形成共振(双击)的交易机会。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份可执行的交易分析报告。报告需要深入分析当前股价走势,评估潜在的上涨空间与风险,并判断其是否具备形成“戴维斯双击”式上涨的潜力。\n\n#### # 输出要求\n1. **图表导向**:分析应基于对价格图表(K线)、成交量和关键技术指标(如均线、MACD、RSI)的解读。\n2. **量化赔率**:明确计算并展示风险回报比(赔率),作为是否值得参与交易的核心依据。\n3. **明确信号**:给出清晰、无歧义的入场、止损和止盈信号。\n4. **客观中立**:只基于当前的市场数据和图表信号进行分析,避免主观臆测。\n5. **Markdown格式**:使用清晰的标题结构。\n\n---\n\n### # 交易策略分析框架\n\n## 一、 当前价格走势与结构分析 (Current Price Action & Structure Analysis)\n\n### 1.1 趋势与动能\n- **当前趋势**:股价目前处于明确的上升、下降还是盘整趋势中?(*参考:关键均线系统,如MA20, MA60, MA120的排列状态*)\n- **关键水平**:当前最重要的支撑位和阻力位分别在哪里?这些是历史高低点、均线位置还是成交密集区?\n- **量价关系**:近期的成交量与价格波动是否匹配?是否存在“价升量增”的健康上涨或“价跌量增”的恐慌抛售?\n\n### 1.2 图表形态\n- 近期是否形成了关键的K线形态?(例如:突破性阳线、反转信号)\n- 是否存在经典的图表形态?(例如:头肩底、W底、收敛三角形、箱体震荡)\n\n## 二、 市场体量与赔率计算 (Market Capacity & Risk/Reward Calculation)\n\n### 2.1 上涨空间评估 (Upside Potential)\n- 如果向上突破关键阻力位,下一个或几个现实的**目标价位**在哪里?(*参考:前期高点、斐波那契扩展位、形态测量目标*)\n- **潜在回报率**:从当前价格到主要目标价位的潜在上涨百分比是多少?\n\n### 2.2 风险评估与止损设置 (Downside Risk & Stop-Loss)\n- 如果交易逻辑被证伪,一个清晰、有效的**止损价位**应该设在哪里?(*参考:关键支撑位下方、上升趋势线下方*)\n- **潜在风险率**:从当前价格到止损价位的潜在下跌百分比是多少?\n\n### 2.3 赔率分析 (Risk/Reward Ratio)\n- 计算**风险回报比**(= 潜在回报率 / 潜在风险率)。这个比率是否具有吸引力?(*专业交易者通常要求至少大于 2:1 或 3:1*)\n- **市场体量**:该股的日均成交额是否足够大,能够容纳计划中的资金进出而不会造成显著的冲击成本?\n\n## 三、 增长路径:“双击”可能性评估 (Growth Path: \"Dual-Click\" Potential)\n\n### 3.1 基本面驱动力 (Fundamental Momentum)\n- 近期是否有或将要有**基本面催化剂**来支撑股价上涨?(*参考《股价催化剂分析》的结论,如:超预期的财报、新产品成功、行业政策利好*)\n- 这个基本面利好是能提供“一次性”的脉冲,还是能开启一个“持续性”的盈利增长周期?\n\n### 3.2 资金面驱动力 (Capital Momentum)\n- 是否有证据表明**增量资金**正在流入?(*参考:成交量的持续放大、机构投资者的增持报告、龙虎榜数据*)\n- 该股所属的板块或赛道,当前是否受到市场主流资金的青睐?\n\n### 3.3 “双击”可能性综合评估\n- 综合来看,公司出现“**业绩超预期(基本面)+ 估值提升(资金面)**”双击局面的可能性有多大?\n- 触发“双击”的关键信号可能是什么?(例如:在发布亮眼财报后,股价以放量涨停的方式突破关键阻力位)\n\n## 四、 交易计划总结 (Actionable Trading Plan)\n\n- **入场信号**:[具体的入场条件。例如:日线收盘价站上 {阻力位A} 并且成交量放大至 {数值X} 以上]\n- **止损策略**:[具体的止损条件。例如:日线收盘价跌破 {支撑位B}]\n- **止盈策略**:[具体的目标位和操作。例如:在 {目标位C} 止盈50%,剩余仓位跟踪止盈]\n- **仓位管理**:[基于赔率和确定性,建议的初始仓位是多少?]\n", + "provider": "alpha_engine" }, "insider_institutional": { "name": "内部人与机构动向分析", - "model": "qwen-flash-2025-07-28", - "prompt_template": "#### # 角色\n你是一位专注于追踪“聪明钱”动向的顶级数据分析师。你对解读上市公司内部人(高管、大股东)的交易行为和机构投资者的持仓变化具有丰富的经验,能够从纷繁的数据中识别出预示未来股价走向的关键信号。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份关于内部人与机构投资者动向的深度分析报告。报告需覆盖**最近6-12个月**的数据,并解读这些“聪明钱”的行为可能暗示的公司前景。\n\n#### # 输出要求\n1. **数据驱动**:分析必须基于公开的、可验证的数据(如交易所披露的内部人交易记录、基金公司的持仓报告如13F文件等)。\n2. **聚焦近期**:重点分析最近6-12个月的动向,以捕捉最新的趋势变化。\n3. **深度解读,而非罗列**:不仅要呈现数据,更要深入分析交易行为背后的动机。例如,区分主动的公开市场增持与被动的股权激励,分析机构的“新进”与“清仓”。\n4. **结合股价**:将内部人和机构的动向与同期的股价走势相结合,分析是否存在“低位吸筹”或“高位派发”的迹象。\n5. **Markdown格式**:使用清晰的标题结构。\n6. **引用来源**:*在分析时需注明数据来源类型,如:来源:Q3季度机构持仓报告*。\n\n---\n\n### # 内部人与机构动向分析框架\n\n## 一、 内部人动向分析 (Insider Activity Analysis)\n\n### 1.1 核心高管交易 (Key Executive Transactions)\n- **公开市场买卖**:近6-12个月,公司的核心高管(CEO, CFO等)是否有在公开市场**主动买入**或**卖出**自家股票?\n- **交易动机解读**:\n - **买入**:买入的金额、次数以及当时股价所处的位置?(*通常,高管在股价下跌后主动增持,被视为强烈的看多信号*)\n - **卖出**:是出于个人资金需求(如纳税)的一次性小额卖出,还是持续、大量的减持?是否在股价历史高位附近减持?\n- **期权行权**:高管行使期权后,是选择继续持有股票,还是立即在市场卖出?\n\n### 1.2 大股东与董事会成员动向 (Major Shareholder & Director Activity)\n- 持股5%以上的大股东或董事会成员,近期的整体趋势是增持还是减持?\n- 是否存在关键股东(如创始人、战略投资者)的持股比例发生重大变化?\n\n### 1.3 内部人持股的总体趋势\n- 综合来看,内部人近半年的行为释放了什么样的集体信号?是信心增强、信心减弱,还是无明显趋势?\n\n## 二、 机构投资者动向分析 (Institutional Investor Activity Analysis)\n\n### 2.1 机构持股的总体变化\n- **持股比例**:机构投资者的总持股占流通股的比例,在最近几个季度是上升还是下降?\n- **股东数量**:持有该公司股票的机构总数是在增加还是减少?(*数量增加通常意味着市场关注度的提升*)\n\n### 2.2 顶级机构的进出 (Top-Tier Institution Moves)\n- **十大机构股东**:当前最大的机构股东有哪些?在最近一个报告期,它们是“增持”、“减持”、“新进”还是“清仓”?\n- **“聪明钱”的踪迹**:是否有以长期价值投资著称的知名基金(如高瓴、景林、Fidelity等)新进入了股东名单,或者大幅增持?\n- 反之,是否有顶级机构在清仓式卖出?\n\n### 2.3 机构观点的“一致性”\n- 从机构的整体行为来看,市场主流机构对该公司的看法是趋于一致(大家都在买或都在卖),还是存在巨大分歧?\n\n## 三、 综合研判:“聪明钱”的信号 (Synthesized Verdict: The \"Smart Money\" Signal)\n\n### 3.1 信号的一致性与背离\n- 内部人和机构投资者的行动方向是否一致?(*例如:内部人增持的同时,顶级机构也在建仓,这是一个极强的看多信号*)\n- “聪明钱”的动向是否与当前市场情绪或股价走势相背离?(*例如:在散户普遍悲观、股价下跌时,内部人和机构却在持续买入*)\n\n### 3.2 最终结论\n- 综合来看,在未来3-6个月,来自“聪明钱”的资金流向是可能成为股价的**顺风**(Tailwind)还是**逆风**(Headwind)?\n" + "model": "deepseek-r1", + "prompt_template": "#### # 角色\n你是一位专注于追踪“聪明钱”动向的顶级数据分析师。你对解读上市公司内部人(高管、大股东)的交易行为和机构投资者的持仓变化具有丰富的经验,能够从纷繁的数据中识别出预示未来股价走向的关键信号。\n\n#### # 任务\n为公司 **{company_name}** (股票代码: **{ts_code}**) 生成一份关于内部人与机构投资者动向的深度分析报告。报告需覆盖**最近6-12个月**的数据,并解读这些“聪明钱”的行为可能暗示的公司前景。\n\n#### # 输出要求\n1. **数据驱动**:分析必须基于公开的、可验证的数据(如交易所披露的内部人交易记录、基金公司的持仓报告如13F文件等)。\n2. **聚焦近期**:重点分析最近6-12个月的动向,以捕捉最新的趋势变化。\n3. **深度解读,而非罗列**:不仅要呈现数据,更要深入分析交易行为背后的动机。例如,区分主动的公开市场增持与被动的股权激励,分析机构的“新进”与“清仓”。\n4. **结合股价**:将内部人和机构的动向与同期的股价走势相结合,分析是否存在“低位吸筹”或“高位派发”的迹象。\n5. **Markdown格式**:使用清晰的标题结构。\n6. **引用来源**:*在分析时需注明数据来源类型,如:来源:Q3季度机构持仓报告*。\n\n---\n\n### # 内部人与机构动向分析框架\n\n## 一、 内部人动向分析 (Insider Activity Analysis)\n\n### 1.1 核心高管交易 (Key Executive Transactions)\n- **公开市场买卖**:近6-12个月,公司的核心高管(CEO, CFO等)是否有在公开市场**主动买入**或**卖出**自家股票?\n- **交易动机解读**:\n - **买入**:买入的金额、次数以及当时股价所处的位置?(*通常,高管在股价下跌后主动增持,被视为强烈的看多信号*)\n - **卖出**:是出于个人资金需求(如纳税)的一次性小额卖出,还是持续、大量的减持?是否在股价历史高位附近减持?\n- **期权行权**:高管行使期权后,是选择继续持有股票,还是立即在市场卖出?\n\n### 1.2 大股东与董事会成员动向 (Major Shareholder & Director Activity)\n- 持股5%以上的大股东或董事会成员,近期的整体趋势是增持还是减持?\n- 是否存在关键股东(如创始人、战略投资者)的持股比例发生重大变化?\n\n### 1.3 内部人持股的总体趋势\n- 综合来看,内部人近半年的行为释放了什么样的集体信号?是信心增强、信心减弱,还是无明显趋势?\n\n## 二、 机构投资者动向分析 (Institutional Investor Activity Analysis)\n\n### 2.1 机构持股的总体变化\n- **持股比例**:机构投资者的总持股占流通股的比例,在最近几个季度是上升还是下降?\n- **股东数量**:持有该公司股票的机构总数是在增加还是减少?(*数量增加通常意味着市场关注度的提升*)\n\n### 2.2 顶级机构的进出 (Top-Tier Institution Moves)\n- **十大机构股东**:当前最大的机构股东有哪些?在最近一个报告期,它们是“增持”、“减持”、“新进”还是“清仓”?\n- **“聪明钱”的踪迹**:是否有以长期价值投资著称的知名基金(如高瓴、景林、Fidelity等)新进入了股东名单,或者大幅增持?\n- 反之,是否有顶级机构在清仓式卖出?\n\n### 2.3 机构观点的“一致性”\n- 从机构的整体行为来看,市场主流机构对该公司的看法是趋于一致(大家都在买或都在卖),还是存在巨大分歧?\n\n## 三、 综合研判:“聪明钱”的信号 (Synthesized Verdict: The \"Smart Money\" Signal)\n\n### 3.1 信号的一致性与背离\n- 内部人和机构投资者的行动方向是否一致?(*例如:内部人增持的同时,顶级机构也在建仓,这是一个极强的看多信号*)\n- “聪明钱”的动向是否与当前市场情绪或股价走势相背离?(*例如:在散户普遍悲观、股价下跌时,内部人和机构却在持续买入*)\n\n### 3.2 最终结论\n- 综合来看,在未来3-6个月,来自“聪明钱”的资金流向是可能成为股价的**顺风**(Tailwind)还是**逆风**(Headwind)?\n", + "provider": "alpha_engine" }, "final_conclusion": { "name": "最终结论", - "model": "qwen-flash-2025-07-28", + "model": "deepseek-r1", "prompt_template": "#### # 角色\n你是一位顶级的基金公司首席投资官(CIO),你的工作不是进行初步研究,而是听取旗下所有分析师(基本面、宏观、技术、新闻、数据等)的报告后,做出最终的、高质量的投资决策。你必须能够穿透信息的迷雾,抓住主要矛盾,并给出明确的行动指令。\n\n#### # 任务\n基于以下七个维度的分析报告(由你的团队提供),为公司 **{company_name}** (股票代码: **{ts_code}**) 形成一份最终的投资决策备忘录。\n\n- **基本面分析**: `{fundamental_analysis}`\n- **看涨分析**: `{bull_case}`\n- **看跌分析**: `{bear_case}`\n- **市场情绪分析**: `{market_analysis}`\n- **新闻催化剂分析**: `{news_analysis}`\n- **交易策略分析**: `{trading_analysis}`\n- **内部人与机构动向**: `{insider_institutional}`\n\n#### # 输出要求\n1. **全局视角**:必须将所有输入信息融会贯通,形成一个逻辑自洽的、立体的投资论点。\n2. **抓住核心**:聚焦于识别当前局面的“核心矛盾”和最大的“预期差”。\n3. **决策导向**:结论必须是明确的、可执行的,并包含对“时机”和“价值”的量化评估。\n4. **精炼语言**:使用专业、果断、直击要害的语言。\n5. **Markdown格式**:使用清晰的标题结构。\n\n---\n\n### # 最终投资决策备忘录\n\n## 一、 核心矛盾与预期差 (Core Contradiction & Expectation Gap)\n\n- **当前的核心矛盾是什么?** 综合所有分析,当前多空双方争论的、最核心的、最关键的一个问题是什么?(例如:是“高估值下的成长故事”与“宏观逆风下的业绩担忧”之间的矛盾?还是“革命性产品”与“商业化落地不确定性”之间的矛盾?)\n- **最大的预期差在哪里?** 我们认为市场在哪一个关键点上可能犯了最大的错误?是我们比市场更乐观,还是更悲观?具体体现在哪个方面?\n\n## 二、 拐点的临近度与关键信号 (Proximity to Inflection Point & Key Signals)\n\n- **拐点是否临近?** 能够解决上述“核心矛盾”的关键催化剂事件,是否即将发生?(参考新闻和催化剂分析)\n- **我们需要验证什么?** 在拐点到来之前,我们需要密切跟踪和验证的、最关键的1-2个数据或信号是什么?(例如:是新产品的预订单数量,还是下一个季度的毛利率指引?)\n\n## 三、 综合投资论点 (Synthesized Investment Thesis)\n\n- **质量与价值(基本面 & 看跌风险)**:这家公司的“质量”如何?它的护城河是否足够深厚,能够在最坏的情况下提供足够的安全边际(清算价值)?\n- **成长与赔率(看涨 & 交易分析)**:如果看涨逻辑兑现,潜在的回报空间有多大?当前的交易结构是否提供了有吸引力的风险回报比?\n- **情绪与资金(市场情绪 & 聪明钱)**:当前的市场情绪是助力还是阻力?“聪明钱”的流向是在支持还是反对我们的判断?\n- **时机与催化剂(新闻分析)**:现在是合适的扣动扳机的时间点吗?还是需要等待某个关键催化剂的出现?\n\n## 四、 最终决策与评级 (Final Decision & Rating)\n\n- **投资结论**:[明确给出:**买入 / 增持 / 观望 / 减持 / 卖出**]\n- **核心投资逻辑**:[用一句话总结本次决策的核心理由]\n\n- **值得参与度评分**:**[请打分, 1-10分]**\n - *(评分标准:1-3分=机会不佳;4-6分=值得观察;7-8分=良好机会,建议配置;9-10分=极佳机会,应重点配置)*\n\n- **关注时间维度**:**[请选择:紧急 / 中期 / 长期]**\n - *(评级标准:**紧急**=关键拐点预计在1个月内;**中期**=关键拐点预计在1-6个月;**长期**=需要持续跟踪6个月以上)*\n", "dependencies": [ "fundamental_analysis", @@ -54,7 +62,8 @@ "news_analysis", "trading_analysis", "insider_institutional" - ] + ], + "provider": "alpha_engine" } } } \ No newline at end of file diff --git a/config/config.json b/config/config.json index cdacdc3..e9383c5 100644 --- a/config/config.json +++ b/config/config.json @@ -1,6 +1,6 @@ { "llm": { - "provider": "new_api", + "provider": "alpha_engine", "gemini": { "base_url": "", "api_key": "YOUR_GEMINI_API_KEY" @@ -8,6 +8,22 @@ "new_api": { "base_url": "http://192.168.3.214:3000/v1", "api_key": "sk-DdTTQ5fdU1aFW6gnYxSNYDgFsVQg938zUcmY4vaB7oPtcNs7" + }, + "alpha_engine": { + "api_url": "http://api-ai-prod.valuesimplex.tech", + "api_key": "api@shangjian!", + "token": "9b5c0b6a5e1e4e8fioouiouqiuioasaz", + "user_id": 999041, + "model": "deepseek-r1", + "using_indicator": true, + "start_time": "2024-01-01", + "doc_show_type": [ + "A001", + "A002", + "A003", + "A004" + ], + "simple_tracking": true } }, "data_sources": { diff --git a/docs/AlphaEngine/1 熵简科技-投研问答API技术文档.pdf b/docs/AlphaEngine/1 熵简科技-投研问答API技术文档.pdf new file mode 100644 index 0000000..84f0bc7 Binary files /dev/null and b/docs/AlphaEngine/1 熵简科技-投研问答API技术文档.pdf differ diff --git a/docs/AlphaEngine/2 test_investment_qa_v3(1).py b/docs/AlphaEngine/2 test_investment_qa_v3(1).py new file mode 100644 index 0000000..173d455 --- /dev/null +++ b/docs/AlphaEngine/2 test_investment_qa_v3(1).py @@ -0,0 +1,99 @@ +# coding:utf-8 +import json +import re + +import requests +from requests.exceptions import ChunkedEncodingError + +# 请求地址 +qa_url = "http://api-ai-prod.valuesimplex.tech/api/v3/finchat" +# 熵简提供的x-api-key +api_key = "api@shangjian!" +token = "9b5c0b6a5e1e4e8fioouiouqiuioasaz" +user_id = 999041 + + +def ask(question, user_id): + # 设置请求头 + headers = { + 'token': token, + 'X-API-KEY': api_key, + 'Content-Type': 'application/json' + } + + # 构造请求体 + payload = json.dumps({ + "msg": question, + # 历史问答,没有则为空List + "history": [], + "user_id": user_id, + "model": "deepseek-r1", # 默认值 不用改 + "using_indicator": True, # 是否用指标 + "start_time": "2024-01-01", # 开始时间 + "doc_show_type": ["A001", "A002", "A003", "A004"], # 文档类型 + "simple_tracking": simple_tracking # 是否简单溯源 + }) + print(f"******开始提问:[{question}]") + + # 发送请求 + response = requests.request("POST", qa_url, data=payload, headers=headers, stream=True) + + qa_result = '' + + # 判断请求是否成功 + if response.status_code == 200: + if stream_enabled: + try: + for chunk in response.iter_content(chunk_size=128): + try: + chunk_event = chunk.decode('utf-8', 'ignore') + except UnicodeDecodeError as e: + # 自定义处理解码错误,例如替换无法解码的部分 + chunk_event = chunk.decode('utf-8', 'replace') + print(f"Decoding error occurred: {e}") + qa_result += chunk_event + print(f"\033[1;32m" + chunk_event) + except ChunkedEncodingError: + print("Stream ended prematurely. Handling gracefully.") + + else: + # 获取响应内容 + qa_result = response.content + # 将响应内容解码为utf-8格式 + qa_result = qa_result.decode('utf-8') + else: + print(f"Failed to get stream data. Status code: {response.status_code}") + # 返回结果 + + return qa_result + + +if __name__ == '__main__': + # 问题内容 + question = '科大讯飞业绩怎么样?' + # 关闭吐字模式 + stream_enabled = True + # 开启简单溯源 + simple_tracking = True + # 调用函数进行问答 + result = ask(question, user_id) + + # 仅打印最终问答结果 + print("**************COT**************") + cot_list = re.findall(r'\{"id":"_cot","content":"(.*?)"}', result) + cot = "".join(cot_list) + print(cot) + print("**********************************") + + # 仅打印最终问答结果 + print("**************最终答案**************") + print(re.findall(r'\{"id":"_final","content":"(.*?)"}', result)[0]) + # print(result['answer']) + print("**********************************") + + if simple_tracking: + print("**************溯源文件**************") + source_file = re.findall(r'\{"id":"tracking_documents","content":\s*(\[[^]]*])}', result) + if source_file and source_file.__len__() > 0: + print(source_file[0]) + print("**********************************") diff --git a/docs/user-guide.md b/docs/user-guide.md index bf85928..331ca05 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -275,3 +275,4 @@ A: + diff --git a/frontend/src/app/config/page.tsx b/frontend/src/app/config/page.tsx index 4f3b349..b9bf042 100644 --- a/frontend/src/app/config/page.tsx +++ b/frontend/src/app/config/page.tsx @@ -27,13 +27,20 @@ export default function ConfigPage() { const [dbUrl, setDbUrl] = useState(''); const [newApiApiKey, setNewApiApiKey] = useState(''); const [newApiBaseUrl, setNewApiBaseUrl] = useState(''); + const [alphaEngineApiUrl, setAlphaEngineApiUrl] = useState(''); + const [alphaEngineApiKey, setAlphaEngineApiKey] = useState(''); + const [alphaEngineToken, setAlphaEngineToken] = useState(''); + const [alphaEngineUserId, setAlphaEngineUserId] = useState(''); const [tushareApiKey, setTushareApiKey] = useState(''); const [finnhubApiKey, setFinnhubApiKey] = useState(''); - // 分析配置的本地状态 + // 全局 LLM 配置 + const [llmProvider, setLlmProvider] = useState(''); + const [llmModel, setLlmModel] = useState(''); + + // 分析配置的本地状态(移除 provider 和 model) const [localAnalysisConfig, setLocalAnalysisConfig] = useState>({}); @@ -52,12 +59,40 @@ export default function ConfigPage() { // 初始化分析配置的本地状态 useEffect(() => { if (analysisConfig?.analysis_modules) { - setLocalAnalysisConfig(analysisConfig.analysis_modules); + // 移除每个模块的 provider 和 model 字段 + const cleanedConfig: typeof localAnalysisConfig = {}; + Object.entries(analysisConfig.analysis_modules).forEach(([key, value]: [string, any]) => { + cleanedConfig[key] = { + name: value.name || '', + prompt_template: value.prompt_template || '', + dependencies: value.dependencies || [] + }; + }); + setLocalAnalysisConfig(cleanedConfig); } }, [analysisConfig]); - // 更新分析配置中的某个字段 - const updateAnalysisField = (type: string, field: 'name' | 'model' | 'prompt_template', value: string) => { + // 初始化全局 LLM 配置(从后端获取) + useEffect(() => { + const loadLlmConfig = async () => { + try { + const response = await fetch('/api/config/llm'); + if (response.ok) { + const data = await response.json(); + setLlmProvider(data.provider || ''); + // 从 provider 配置中获取 model + const providerConfig = data.config || {}; + setLlmModel(providerConfig.model || ''); + } + } catch (e) { + console.error('Failed to load LLM config:', e); + } + }; + loadLlmConfig(); + }, []); + + // 更新分析配置中的某个字段(移除 provider 和 model) + const updateAnalysisField = (type: string, field: 'name' | 'prompt_template', value: string) => { setLocalAnalysisConfig(prev => ({ ...prev, [type]: { @@ -161,6 +196,15 @@ export default function ConfigPage() { }; } + if (alphaEngineApiUrl || alphaEngineApiKey || alphaEngineToken) { + newConfig.alpha_engine = { + api_url: alphaEngineApiUrl || config?.alpha_engine?.api_url || '', + api_key: alphaEngineApiKey || config?.alpha_engine?.api_key || '', + token: alphaEngineToken || config?.alpha_engine?.token || '', + user_id: alphaEngineUserId ? parseInt(alphaEngineUserId) : (config?.alpha_engine?.user_id || 999041), + }; + } + if (tushareApiKey || finnhubApiKey) { newConfig.data_sources = { ...config?.data_sources, @@ -216,10 +260,23 @@ export default function ConfigPage() { handleTest('finnhub', { api_key: finnhubApiKey || config?.data_sources?.finnhub?.api_key }); }; + const handleTestAlphaEngine = () => { + handleTest('alpha_engine', { + api_url: alphaEngineApiUrl || config?.alpha_engine?.api_url, + api_key: alphaEngineApiKey || config?.alpha_engine?.api_key, + token: alphaEngineToken || config?.alpha_engine?.token, + user_id: alphaEngineUserId ? parseInt(alphaEngineUserId) : (config?.alpha_engine?.user_id || 999041) + }); + }; + const handleReset = () => { setDbUrl(''); setNewApiApiKey(''); setNewApiBaseUrl(''); + setAlphaEngineApiUrl(''); + setAlphaEngineApiKey(''); + setAlphaEngineToken(''); + setAlphaEngineUserId(''); setTushareApiKey(''); setFinnhubApiKey(''); setTestResults({}); @@ -345,41 +402,188 @@ export default function ConfigPage() { AI 服务配置 - New API 设置 (兼容 OpenAI 格式) + 配置大模型服务提供商和全局设置 - -
- -
- setNewApiApiKey(e.target.value)} - placeholder="留空表示保持当前值" - className="flex-1" - /> - + {saveMessage && saveMessage.includes('LLM') && ( + + {saveMessage} + + )}
- {testResults.new_api && ( - - {testResults.new_api.message} - - )}
- -
- - setNewApiBaseUrl(e.target.value)} - placeholder="例如: http://localhost:3000/v1" - className="flex-1" - /> + + + +
+
+ +

兼容 OpenAI API 格式的服务

+
+
+ +
+ setNewApiApiKey(e.target.value)} + placeholder="留空表示保持当前值" + className="flex-1" + /> + +
+ {testResults.new_api && ( + + {testResults.new_api.message} + + )} +
+ +
+ + setNewApiBaseUrl(e.target.value)} + placeholder="例如: http://localhost:3000/v1" + className="flex-1" + /> +
+
+
+ + + +
+ +

熵简科技投研问答 API

+
+
+ + setAlphaEngineApiUrl(e.target.value)} + placeholder="例如: http://api-ai-prod.valuesimplex.tech" + className="flex-1" + /> +
+
+ +
+ setAlphaEngineApiKey(e.target.value)} + placeholder="留空表示保持当前值" + className="flex-1" + /> + +
+ {testResults.alpha_engine && ( + + {testResults.alpha_engine.message} + + )} +
+
+ + setAlphaEngineToken(e.target.value)} + placeholder="留空表示保持当前值" + className="flex-1" + /> +
+
+ + setAlphaEngineUserId(e.target.value)} + placeholder="默认: 999041" + className="flex-1" + /> +
+
+
@@ -470,19 +674,6 @@ export default function ConfigPage() { />
-
- - updateAnalysisField(type, 'model', e.target.value)} - placeholder="例如: gemini-1.5-pro" - /> -

- 在 AI 服务中配置的模型名称 -

-
-
diff --git a/frontend/src/app/fonts/README.md b/frontend/src/app/fonts/README.md index e5d7927..91fe2c7 100644 --- a/frontend/src/app/fonts/README.md +++ b/frontend/src/app/fonts/README.md @@ -21,3 +21,4 @@ + diff --git a/frontend/src/app/report/[symbol]/page.tsx b/frontend/src/app/report/[symbol]/page.tsx index 079042e..443df52 100644 --- a/frontend/src/app/report/[symbol]/page.tsx +++ b/frontend/src/app/report/[symbol]/page.tsx @@ -109,6 +109,9 @@ export default function ReportPage() { const [saving, setSaving] = useState(false) const [saveMsg, setSaveMsg] = useState(null) + + // TradingView 显示控制 + const [showTradingView, setShowTradingView] = useState(false) const saveReport = async () => { try { @@ -155,6 +158,9 @@ export default function ReportPage() { return; } + // 标记已触发分析 + fullAnalysisTriggeredRef.current = true; + // 初始化/重置状态,准备顺序执行 stopRequestedRef.current = false; abortControllerRef.current?.abort(); @@ -182,12 +188,7 @@ export default function ReportPage() { setManualRunKey((k) => k + 1); }; - useEffect(() => { - if (financials && !fullAnalysisTriggeredRef.current) { - fullAnalysisTriggeredRef.current = true; - runFullAnalysis(); - } - }, [financials]); + // 移除自动开始分析的逻辑,改为手动触发 // 计算完成比例 const completionProgress = useMemo(() => { @@ -796,23 +797,53 @@ export default function ReportPage() { -

股价图表(来自 TradingView)

-
- -
- 实时股价图表 - {unifiedSymbol} -
+
+

股价图表(来自 TradingView)

+
- + {showTradingView ? ( + <> +
+ +
+ 实时股价图表 - {unifiedSymbol} +
+
+ + + ) : ( +
+
+

点击"显示图表"按钮加载 TradingView 股价图表

+

图表数据来自 TradingView

+
+
+ )} -

财务数据

+
+

财务数据

+ {financials && !fullAnalysisTriggeredRef.current && analysisConfig?.analysis_modules && ( + + )} +
{isLoading ? ( @@ -834,6 +865,25 @@ export default function ReportPage() {
)} + {financials && !fullAnalysisTriggeredRef.current && analysisConfig?.analysis_modules && ( +
+
+
+

准备开始分析

+

+ 财务数据已加载完成。点击"开始分析"按钮启动大模型分析流程。 +

+
+ +
+
+ )} + {financials && ( diff --git a/frontend/src/lib/prisma.ts b/frontend/src/lib/prisma.ts index 050d673..64656e2 100644 --- a/frontend/src/lib/prisma.ts +++ b/frontend/src/lib/prisma.ts @@ -43,3 +43,4 @@ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma +