From 0ee0c753a3f1c7293c868f62ac62140c6fdc93c1 Mon Sep 17 00:00:00 2001 From: father Date: Tue, 4 Aug 2026 13:49:51 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=80=8E=E5=8A=A0=E6=9B=B4=E5=A4=9Aprovide?= =?UTF-8?q?r=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/main.py | 18 +- cli/utils.py | 48 +++- config.yaml | 37 ++- config_us.yaml | 4 +- contest_trade/config/config.py | 22 +- .../contest/researcher/research_contest.py | 14 +- contest_trade/models/llm_model.py | 141 ++++++--- contest_trade/models/provider_auth.py | 269 ++++++++++++++++++ requirements.txt | 3 +- 9 files changed, 486 insertions(+), 70 deletions(-) create mode 100644 contest_trade/models/provider_auth.py diff --git a/cli/main.py b/cli/main.py index 05c108c..e4728cf 100644 --- a/cli/main.py +++ b/cli/main.py @@ -1,12 +1,28 @@ """ ContestTrade: 基于内部竞赛机制的Multi-Agent交易系统 """ +from pathlib import Path + + +def _bootstrap_dotenv() -> None: + """Load .env before any config/LLM imports.""" + env_path = Path(__file__).resolve().parent.parent / ".env" + if not env_path.is_file(): + return + try: + from dotenv import load_dotenv + load_dotenv(env_path, override=False) + except ImportError: + pass + + +_bootstrap_dotenv() + import asyncio import sys import json import re import os -from pathlib import Path from typing import Optional, Dict from datetime import datetime from collections import deque diff --git a/cli/utils.py b/cli/utils.py index b2f4794..2074e2f 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -57,16 +57,52 @@ def validate_tushare_connection(): def validate_llm_connection(): """验证LLM连接""" try: - # Import LLM model when needed - from contest_trade.models.llm_model import GLOBAL_LLM - + from contest_trade.models.llm_model import GLOBAL_LLM, GLOBAL_LLM_CONFIG + from contest_trade.models.provider_auth import ( + format_missing_key_error, + get_env_config_hints, + get_env_var_candidates, + resolve_api_key, + ) + console.print("🔍 [cyan]正在验证LLM配置...[/cyan]") + console.print( + f" Provider: {GLOBAL_LLM_CONFIG.provider} | " + f"Model: Kimi K3 ({GLOBAL_LLM_CONFIG.model_name}) | " + f"Base URL: {GLOBAL_LLM_CONFIG.base_url or '—'}" + ) + + resolved_key = resolve_api_key( + GLOBAL_LLM_CONFIG.provider, + config_key=GLOBAL_LLM_CONFIG.api_key, + base_url=GLOBAL_LLM_CONFIG.base_url, + ) + if not resolved_key: + env_vars = get_env_var_candidates( + GLOBAL_LLM_CONFIG.provider, + GLOBAL_LLM_CONFIG.base_url, + ) + console.print( + f"❌ [red]{format_missing_key_error(GLOBAL_LLM_CONFIG.provider, GLOBAL_LLM_CONFIG.base_url)}[/red]" + ) + console.print(f" 请在 .env 中设置: {', '.join(env_vars[:3])}") + for hint in get_env_config_hints( + GLOBAL_LLM_CONFIG.provider, + GLOBAL_LLM_CONFIG.base_url, + ): + console.print(f" ⚠️ [yellow]{hint}[/yellow]") + return False + test_messages = [ {"role": "user", "content": "请回复'连接测试成功',不要添加任何其他内容。"} ] - result = GLOBAL_LLM.run(test_messages, max_tokens=1, temperature=0.1, max_retries=0) - if result and hasattr(result, 'content') and result.content: - console.print(f"✅ [green]LLM连接成功[/green] - 模型: {GLOBAL_LLM.model_name}") + result = GLOBAL_LLM.run(test_messages, max_tokens=32, temperature=1, max_retries=0) + has_content = result and ( + (hasattr(result, 'content') and result.content) + or (hasattr(result, 'reasoning_content') and result.reasoning_content) + ) + if has_content: + console.print(f"✅ [green]LLM连接成功[/green] - 模型: Kimi K3 ({GLOBAL_LLM.model_name})") return True else: console.print("❌ [red]LLM连接失败 - 无响应内容[/red]") diff --git a/config.yaml b/config.yaml index b23e59a..7dead8f 100644 --- a/config.yaml +++ b/config.yaml @@ -23,12 +23,28 @@ finnhub_key: "" # # 配置示例 / Configuration examples: # +# 也可通过 .env 或环境变量设置 API Key(优先级低于 config.yaml): +# 1. 复制 .env.example 为 .env 并填入密钥 +# 2. DeepSeek -> DEEPSEEK_API_KEY +# 3. StepFun -> STEPFUN_API_KEY +# 4. OpenAI -> OPENAI_API_KEY +# 5. 通用 -> LLM_API_KEY +# # OpenAI: # provider: "openai" # base_url: "https://api.openai.com/v1" # api_key: "your_openai_api_key" # model_name: "gpt-4" # +# Kimi Code (OpenAI compatible): +# provider: "openai" +# base_url: "https://api.kimi.com/coding/v1" +# api_key: null # 在 .env 中设置 KIMI_API_KEY(Kimi Code 控制台密钥) +# model_name: "kimi-k3" # API model id;显示名称: Kimi K3 +# +# Kimi Open Platform: +# base_url: "https://api.moonshot.ai/v1" +# model_name: "kimi-k3" # DeepSeek (OpenAI compatible): # provider: "openai" # base_url: "https://api.deepseek.com" @@ -56,24 +72,21 @@ finnhub_key: "" llm: provider: "openai" - base_url: "https://api.deepseek.com" - #YOUR_DEEPSEEK_API_KEY - api_key: null - model_name: "deepseek-chat" + base_url: "https://api.kimi.com/coding/v1" + api_key: null + model_name: "kimi-k3" llm_thinking: provider: "openai" - base_url: "https://api.deepseek.com" - #YOUR_DEEPSEEK_API_KEY - api_key: null - model_name: "deepseek-reasoner" + base_url: "https://api.kimi.com/coding/v1" + api_key: null + model_name: "kimi-k3" vlm: provider: "openai" - base_url: "https://api.stepfun.com/v1" - #YOUR_STEPFUN_API_KEY - api_key: null - model_name: "step-1o-turbo-vision" + base_url: "https://api.kimi.com/coding/v1" + api_key: null + model_name: "kimi-k3" data_agents_config: diff --git a/config_us.yaml b/config_us.yaml index 4393f46..37447b7 100644 --- a/config_us.yaml +++ b/config_us.yaml @@ -16,9 +16,11 @@ finnhub_key: "" #YOUR_ALPHA_VANTAGE_KEY alpha_vantage_key: "" -# YOUR_POLYGON_KEY +#YOUR_POLYGON_KEY polygon_key: "" +# API keys: set in config.yaml, or copy .env.example to .env (see DEEPSEEK_API_KEY, STEPFUN_API_KEY, LLM_API_KEY) + llm: provider: "openai" base_url: "https://api.deepseek.com" diff --git a/contest_trade/config/config.py b/contest_trade/config/config.py index 0e229ee..55f1458 100644 --- a/contest_trade/config/config.py +++ b/contest_trade/config/config.py @@ -6,6 +6,26 @@ import os PROJECT_ROOT = Path(__file__).parent.parent.resolve() +REPO_ROOT = PROJECT_ROOT.parent + + +def _load_dotenv() -> None: + """ + Load .env from project root. Existing shell env vars take precedence. + + @generated AI Assistant - 2026-08-04 11:42:00 + """ + env_path = REPO_ROOT / ".env" + if not env_path.is_file(): + return + try: + from dotenv import load_dotenv + load_dotenv(env_path, override=False) + except ImportError: + print(f"Warning: .env found at {env_path} but python-dotenv is not installed.") + + +_load_dotenv() class ProjectConfig: @@ -20,7 +40,7 @@ def __init__(self) -> None: else: config_filename = "config.yaml" - yaml_path = PROJECT_ROOT.parent / config_filename + yaml_path = REPO_ROOT / config_filename print(f"Loading config from: {yaml_path} (Market: {market_type})") with open(yaml_path, "r", encoding="utf-8") as fr: diff --git a/contest_trade/contest/researcher/research_contest.py b/contest_trade/contest/researcher/research_contest.py index 099b0f6..8416c89 100644 --- a/contest_trade/contest/researcher/research_contest.py +++ b/contest_trade/contest/researcher/research_contest.py @@ -19,6 +19,7 @@ sys.path.append(str(PROJECT_ROOT)) from models.llm_model import GLOBAL_LLM +from models.provider_auth import resolve_api_key, resolve_base_url, resolve_chat_completions_url from utils.market_manager import GLOBAL_MARKET_MANAGER from config.config import cfg from research_contest_types import SignalData, ResearchContestResult @@ -316,10 +317,17 @@ async def _get_current_judge_scores(self, current_signals: Dict[str, SignalData] """获取当天信号的judge评分""" logger.info(f"获取当天信号judge评分 - {len(current_signals)} 个信号") + llm_section = cfg.llm if isinstance(cfg.llm, dict) else vars(cfg.llm) + provider = llm_section.get("provider", "openai") + base_url = resolve_base_url(provider, llm_section.get("base_url")) llm_config = { - "api_key": cfg.llm.api_key, - "api_base": cfg.llm.api_base, - "model_name": cfg.llm.model_name + "api_key": resolve_api_key( + provider, + config_key=llm_section.get("api_key"), + base_url=base_url, + ), + "api_base": resolve_chat_completions_url(base_url), + "model_name": llm_section.get("model_name"), } judge_scores = await self.signal_judger.judge_signals( diff --git a/contest_trade/models/llm_model.py b/contest_trade/models/llm_model.py index f48254c..ec198d6 100644 --- a/contest_trade/models/llm_model.py +++ b/contest_trade/models/llm_model.py @@ -19,6 +19,14 @@ sys.path.append(str(PROJECT_ROOT)) from config.config import cfg +from models.provider_auth import ( + format_missing_key_error, + is_openai_compatible, + normalize_secret, + normalize_temperature, + resolve_api_key, + resolve_base_url, +) from models.base_agent_model import ( BaseAgentModel, AsyncResponseStream, @@ -33,6 +41,18 @@ class ProviderType(Enum): GEMINI = "gemini" OLLAMA = "ollama" + +def _normalize_provider(provider: str) -> str: + """Map provider aliases to transport provider.""" + normalized = (provider or "openai").lower().strip() + if is_openai_compatible(normalized): + return ProviderType.OPENAI.value + if normalized == ProviderType.GEMINI.value: + return ProviderType.GEMINI.value + if normalized == ProviderType.OLLAMA.value: + return ProviderType.OLLAMA.value + return normalized + class LLMModelConfig: def __init__(self, provider: str, model_name: str, api_key: str = None, base_url: str = None, max_retries: int = 3, retry_delay: float = 20.0, timeout: float = 60.0, @@ -93,11 +113,13 @@ def __init__(self, config: LLMModelConfig): except ImportError: raise ImportError("openai package is required for OpenAI provider. Install with: pip install openai") - # Set default values from environment if not provided - if self.api_key is None: - self.api_key = os.environ.get("OPENAI_API_KEY") - if self.base_url is None: - self.base_url = os.environ.get("OPENAI_BASE_URL") + # Resolve credentials: config -> provider env vars -> generic fallback + self.api_key = resolve_api_key( + config.provider, + config_key=self.api_key, + base_url=self.base_url, + ) + self.base_url = resolve_base_url(config.provider, config_base_url=self.base_url) # Initialize clients (lazy initialization if no API key) if self.api_key: @@ -125,7 +147,10 @@ def _ensure_clients(self): """Ensure OpenAI clients are initialized.""" if self.async_client is None: if not self.api_key: - raise ValueError("OpenAI API key is required but not provided. Set it in config or OPENAI_API_KEY environment variable.") + raise ValueError(format_missing_key_error( + self.config.provider, + base_url=self.base_url, + )) self.client = OpenAI( api_key=self.api_key, @@ -147,6 +172,7 @@ async def create_stream(self, messages: List[Dict[str, str]], temperature: float max_tokens: Optional[int], **kwargs) -> Any: self._ensure_clients() # Ensure clients are initialized processed_messages = self.preprocess_messages(messages) + temperature = normalize_temperature(self.base_url, temperature) params = { "model": self.model_name, @@ -200,12 +226,19 @@ def __init__(self, config: LLMModelConfig): except ImportError: raise ImportError("google-generativeai package is required for Gemini provider. Install with: pip install google-generativeai") - # Set API key - if self.api_key is None: - self.api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY") - - if self.api_key: - genai.configure(api_key=self.api_key) + self.api_key = resolve_api_key( + ProviderType.GEMINI.value, + config_key=self.api_key, + base_url=self.base_url, + ) + if not self.api_key: + raise ValueError(format_missing_key_error( + ProviderType.GEMINI.value, + base_url=self.base_url, + config_path="llm.api_key", + )) + + genai.configure(api_key=self.api_key) # Configure model generation_config = { @@ -287,13 +320,15 @@ class OllamaProvider(BaseProvider): def __init__(self, config: LLMModelConfig): super().__init__(config) - # Set default base URL for Ollama - if self.base_url is None: - self.base_url = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434") - - # Ollama doesn't typically require API keys, but support it if provided - if self.api_key is None: - self.api_key = os.environ.get("OLLAMA_API_KEY") + self.base_url = resolve_base_url( + ProviderType.OLLAMA.value, + config_base_url=self.base_url, + ) or "http://localhost:11434" + self.api_key = resolve_api_key( + ProviderType.OLLAMA.value, + config_key=self.api_key, + base_url=self.base_url, + ) async def create_stream(self, messages: List[Dict[str, str]], temperature: float, max_tokens: Optional[int], **kwargs) -> Any: @@ -359,8 +394,8 @@ def process_chunk(self, chunk_line: str) -> StreamingChunk[str]: def create_provider(config: LLMModelConfig) -> BaseProvider: """Factory function to create the appropriate provider based on config.""" - provider_type = config.provider.lower() - + provider_type = _normalize_provider(config.provider) + if provider_type == ProviderType.OPENAI.value: return OpenAIProvider(config) elif provider_type == ProviderType.GEMINI.value: @@ -368,7 +403,8 @@ def create_provider(config: LLMModelConfig) -> BaseProvider: elif provider_type == ProviderType.OLLAMA.value: return OllamaProvider(config) else: - raise ValueError(f"Unsupported provider: {provider_type}. Supported providers: {[p.value for p in ProviderType]}") + supported = sorted({p.value for p in ProviderType} | {"deepseek", "stepfun", "moonshot", "qwen", "zhipu"}) + raise ValueError(f"Unsupported provider: {config.provider}. Supported providers: {supported}") class LLMModel(BaseAgentModel): @@ -669,39 +705,54 @@ def detect_provider(model_name: str, base_url: str = None) -> str: return ProviderType.OPENAI.value +def _build_llm_config(section: dict, fallback_section: dict = None) -> LLMModelConfig: + """Build LLMModelConfig with provider-aware credential resolution.""" + provider = section.get("provider", detect_provider(section["model_name"], section.get("base_url"))) + base_url = section.get("base_url") + api_key = resolve_api_key( + provider, + config_key=normalize_secret(section.get("api_key")), + base_url=base_url, + ) + if api_key is None and fallback_section: + api_key = resolve_api_key( + fallback_section.get("provider", provider), + config_key=normalize_secret(fallback_section.get("api_key")), + base_url=fallback_section.get("base_url"), + ) + return LLMModelConfig( + provider=provider, + model_name=section["model_name"], + api_key=api_key, + base_url=resolve_base_url(provider, base_url) or base_url, + ) + + # Create global configurations with auto-detected providers -llm_provider = cfg.llm.get("provider", detect_provider(cfg.llm["model_name"], cfg.llm.get("base_url"))) -GLOBAL_LLM_CONFIG = LLMModelConfig( - provider=llm_provider, - model_name=cfg.llm["model_name"], - api_key=cfg.llm.get("api_key"), - base_url=cfg.llm.get("base_url") -) +GLOBAL_LLM_CONFIG = _build_llm_config(cfg.llm) GLOBAL_LLM = LLMModel(GLOBAL_LLM_CONFIG) try: - thinking_provider = cfg.llm_thinking.get("provider", detect_provider(cfg.llm_thinking["model_name"], cfg.llm_thinking.get("base_url"))) - GLOBAL_THINKING_LLM_CONFIG = LLMModelConfig( - provider=thinking_provider, - model_name=cfg.llm_thinking["model_name"], - api_key=cfg.llm_thinking.get("api_key"), - base_url=cfg.llm_thinking.get("base_url") - ) - assert GLOBAL_THINKING_LLM_CONFIG.api_key is not None + GLOBAL_THINKING_LLM_CONFIG = _build_llm_config(cfg.llm_thinking, fallback_section=cfg.llm) + if GLOBAL_THINKING_LLM_CONFIG.api_key is None: + raise ValueError(format_missing_key_error( + GLOBAL_THINKING_LLM_CONFIG.provider, + base_url=GLOBAL_THINKING_LLM_CONFIG.base_url, + config_path="llm_thinking.api_key", + )) GLOBAL_THINKING_LLM = LLMModel(GLOBAL_THINKING_LLM_CONFIG) except Exception as e: print(f"加载thinking模型失败,使用llm模型替代: {e}") GLOBAL_THINKING_LLM = GLOBAL_LLM try: - vlm_provider = cfg.vlm.get("provider", detect_provider(cfg.vlm["model_name"], cfg.vlm.get("base_url"))) - GLOBAL_VLM_CONFIG = LLMModelConfig( - provider=vlm_provider, - model_name=cfg.vlm["model_name"], - api_key=cfg.vlm.get("api_key"), - base_url=cfg.vlm.get("base_url") - ) - assert GLOBAL_VLM_CONFIG.api_key is not None + GLOBAL_VLM_CONFIG = _build_llm_config(cfg.vlm, fallback_section=cfg.llm) + if GLOBAL_VLM_CONFIG.api_key is None: + raise ValueError(format_missing_key_error( + GLOBAL_VLM_CONFIG.provider, + base_url=GLOBAL_VLM_CONFIG.base_url, + config_path="vlm.api_key", + )) GLOBAL_VISION_LLM = LLMModel(GLOBAL_VLM_CONFIG) except Exception as e: print(f"加载vlm模型失败,vision能力不可用: {e}") diff --git a/contest_trade/models/provider_auth.py b/contest_trade/models/provider_auth.py new file mode 100644 index 0000000..6bd2abb --- /dev/null +++ b/contest_trade/models/provider_auth.py @@ -0,0 +1,269 @@ +""" +Provider authentication resolution for multi-LLM support. + +Inspired by openclaw's layered auth resolution: config -> provider env vars -> generic fallback. +""" +import os +from typing import Optional, Sequence +from urllib.parse import urlparse + +# Generic fallback env var (checked after provider-specific vars) +GENERIC_LLM_API_KEY_ENV = "LLM_API_KEY" + +# Provider -> ordered env var candidates (first non-empty wins) +PROVIDER_AUTH_ENV_VAR_CANDIDATES: dict[str, tuple[str, ...]] = { + "openai": ("OPENAI_API_KEY",), + "deepseek": ("DEEPSEEK_API_KEY", "OPENAI_API_KEY"), + "stepfun": ("STEPFUN_API_KEY", "OPENAI_API_KEY"), + "gemini": ("GEMINI_API_KEY", "GOOGLE_API_KEY"), + "ollama": ("OLLAMA_API_KEY",), + "anthropic": ("ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"), + "qwen": ("DASHSCOPE_API_KEY", "QWEN_API_KEY"), + "kimi": ("KIMI_API_KEY", "MOONSHOT_API_KEY", "OPENAI_API_KEY"), + "moonshot": ("MOONSHOT_API_KEY", "KIMI_API_KEY", "OPENAI_API_KEY"), + "zhipu": ("ZHIPU_API_KEY", "OPENAI_API_KEY"), +} + +# base_url host patterns -> logical provider id for env var lookup +BASE_URL_PROVIDER_PATTERNS: tuple[tuple[str, str], ...] = ( + ("deepseek.com", "deepseek"), + ("stepfun", "stepfun"), + ("dashscope", "qwen"), + ("api.kimi.com", "kimi"), + ("moonshot.ai", "moonshot"), + ("moonshot.cn", "moonshot"), + ("open.bigmodel.cn", "zhipu"), + ("generativelanguage.googleapis.com", "gemini"), + ("anthropic.com", "anthropic"), +) + +# OpenAI-compatible provider aliases -> transport provider +OPENAI_COMPATIBLE_ALIASES: frozenset[str] = frozenset({ + "openai", + "deepseek", + "stepfun", + "kimi", + "moonshot", + "zhipu", + "qwen", + "anthropic-openai", +}) + +# Provider-specific base URL env vars +PROVIDER_BASE_URL_ENV: dict[str, str] = { + "openai": "OPENAI_BASE_URL", + "deepseek": "DEEPSEEK_BASE_URL", + "stepfun": "STEPFUN_BASE_URL", + "kimi": "KIMI_BASE_URL", + "moonshot": "MOONSHOT_BASE_URL", + "ollama": "OLLAMA_BASE_URL", +} + + +def normalize_secret(value: Optional[str]) -> Optional[str]: + """Treat null/empty/whitespace-only values as missing.""" + if value is None: + return None + if not isinstance(value, str): + return str(value) if value else None + stripped = value.strip() + return stripped if stripped else None + + +def detect_provider_from_base_url(base_url: Optional[str]) -> Optional[str]: + """Infer logical provider id from base_url host.""" + if not base_url: + return None + host = (urlparse(base_url).netloc or base_url).lower() + for pattern, provider_id in BASE_URL_PROVIDER_PATTERNS: + if pattern in host: + return provider_id + return None + + +def resolve_auth_provider_id(provider: str, base_url: Optional[str] = None) -> str: + """Resolve the provider id used for env var lookup.""" + normalized = (provider or "openai").lower().strip() + detected = detect_provider_from_base_url(base_url) + if detected: + return detected + if normalized in OPENAI_COMPATIBLE_ALIASES or normalized in PROVIDER_AUTH_ENV_VAR_CANDIDATES: + return normalized + return normalized + + +def is_openai_compatible(provider: str) -> bool: + """Whether the provider uses OpenAI-compatible API transport.""" + normalized = (provider or "").lower().strip() + return normalized in OPENAI_COMPATIBLE_ALIASES or normalized == "openai" + + +def _first_env_value(env_vars: Sequence[str]) -> Optional[str]: + for env_var in env_vars: + value = normalize_secret(os.environ.get(env_var)) + if value: + return value + return None + + +def get_env_var_candidates(provider: str, base_url: Optional[str] = None) -> list[str]: + """Return ordered env var names to try for API key resolution.""" + auth_provider = resolve_auth_provider_id(provider, base_url) + detected = detect_provider_from_base_url(base_url) + candidates: list[str] = [] + + # When base_url indicates a specific provider, prefer its env vars first + if detected: + for env_var in PROVIDER_AUTH_ENV_VAR_CANDIDATES.get(detected, ()): + if env_var not in candidates: + candidates.append(env_var) + + for env_var in PROVIDER_AUTH_ENV_VAR_CANDIDATES.get(auth_provider, ()): + if env_var not in candidates: + candidates.append(env_var) + + if GENERIC_LLM_API_KEY_ENV not in candidates: + candidates.append(GENERIC_LLM_API_KEY_ENV) + + if is_openai_compatible(provider) or is_openai_compatible(auth_provider): + if "OPENAI_API_KEY" not in candidates: + candidates.append("OPENAI_API_KEY") + + return candidates + + +def resolve_api_key( + provider: str, + config_key: Optional[str] = None, + base_url: Optional[str] = None, +) -> Optional[str]: + """ + Resolve API key with priority: config -> provider env vars -> generic fallback. + + @generated AI Assistant - 2026-08-04 11:35:00 + """ + key = normalize_secret(config_key) + if key: + return key + return _first_env_value(get_env_var_candidates(provider, base_url)) + + +def resolve_base_url( + provider: str, + config_base_url: Optional[str] = None, +) -> Optional[str]: + """ + Resolve base URL from config or provider-specific env var. + + @generated AI Assistant - 2026-08-04 11:35:00 + """ + url = normalize_secret(config_base_url) + if url: + return url + + auth_provider = resolve_auth_provider_id(provider, config_base_url) + env_var = PROVIDER_BASE_URL_ENV.get(auth_provider) + if env_var: + return normalize_secret(os.environ.get(env_var)) + return None + + +def normalize_temperature(base_url: Optional[str], temperature: float) -> float: + """ + Kimi Code API only accepts temperature=1 for some models. + + @generated AI Assistant - 2026-08-04 13:35:00 + """ + if base_url and "api.kimi.com" in base_url and temperature != 1.0: + return 1.0 + return temperature + + +def resolve_chat_completions_url(base_url: Optional[str]) -> str: + """ + Build OpenAI-compatible chat completions endpoint URL for raw HTTP clients. + + @generated AI Assistant - 2026-08-04 11:35:00 + """ + if not base_url: + raise ValueError("base_url is required to build chat completions URL") + + url = base_url.rstrip("/") + if url.endswith("/chat/completions"): + return url + if url.endswith("/v1"): + return f"{url}/chat/completions" + if "deepseek.com" in url or "api.kimi.com" in url: + return f"{url}/chat/completions" + return f"{url}/v1/chat/completions" + + +def get_env_config_hints( + provider: str, + base_url: Optional[str] = None, +) -> list[str]: + """ + Detect common .env misconfiguration (wrong var name or provider mismatch). + + @generated AI Assistant - 2026-08-04 11:45:00 + """ + hints: list[str] = [] + needed = get_env_var_candidates(provider, base_url) + needed_set = set(needed) + detected = detect_provider_from_base_url(base_url) + + for env_var in needed: + raw = os.environ.get(env_var) + if raw is not None and raw.strip() == "": + hints.append(f".env 中 `{env_var}=` 为空,请填入密钥或删除该行") + + for env_var, value in os.environ.items(): + if env_var in needed_set or not env_var.endswith("_API_KEY"): + continue + secret = normalize_secret(value) + if not secret: + continue + if env_var == "GEMINI_API_KEY" and secret.startswith("sk-kimi-"): + hints.append( + "检测到 `GEMINI_API_KEY` 中是 Kimi 密钥(sk-kimi-)," + "当前 config 使用的是 DeepSeek。请改用 `KIMI_API_KEY` 或 `MOONSHOT_API_KEY`," + "并将 config.yaml 的 base_url 改为 https://api.kimi.com/coding/v1" + ) + elif env_var not in needed_set and env_var != GENERIC_LLM_API_KEY_ENV: + hints.append( + f"`.env` 中已设置 `{env_var}`,但当前 provider 不会读取它;" + f"请改用: {', '.join(needed[:3])}" + ) + + if detected == "deepseek" and normalize_secret(os.environ.get("KIMI_API_KEY")): + hints.append( + "已设置 `KIMI_API_KEY`,但 config 指向 DeepSeek;" + "请设置 `DEEPSEEK_API_KEY` 或 `LLM_API_KEY`,或改用 Moonshot 配置" + ) + + return hints + + +def format_missing_key_error( + provider: str, + base_url: Optional[str] = None, + config_path: str = "llm.api_key", +) -> str: + """ + Build a provider-aware error message listing env var options. + + @generated AI Assistant - 2026-08-04 11:35:00 + """ + auth_provider = resolve_auth_provider_id(provider, base_url) + env_vars = get_env_var_candidates(provider, base_url) + env_hint = " / ".join(f"`{v}`" for v in env_vars[:4]) + + detected = detect_provider_from_base_url(base_url) + provider_label = auth_provider + if detected and detected != auth_provider: + provider_label = f"{auth_provider} (detected: {detected})" + + return ( + f"API key required for provider '{provider_label}' but not provided. " + f"Set `{config_path}` in config.yaml, or export one of: {env_hint}." + ) diff --git a/requirements.txt b/requirements.txt index 4645a64..1964a4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,5 @@ rich tabulate akshare crawl4ai -lightgbm \ No newline at end of file +lightgbm +python-dotenv \ No newline at end of file From 0664186962f68d74c2fc134b469ef73a58a50e79 Mon Sep 17 00:00:00 2001 From: father Date: Wed, 5 Aug 2026 08:23:55 +0800 Subject: [PATCH 2/2] update --- .env.example | 34 ++ config.yaml | 10 +- contest_trade/data_source/sina_news_crawl.py | 4 +- contest_trade/models/llm_model.py | 2 +- .../cache/market_manager/trade_calendar.json | 494 +----------------- contest_trade/utils/market_manager.py | 71 ++- 6 files changed, 88 insertions(+), 527 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0141304 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# Copy to .env and fill in your keys: +# cp .env.example .env + +# LLM API Keys — 变量名须与 config.yaml 中的 base_url 对应: +# DeepSeek -> DEEPSEEK_API_KEY +# Kimi Code -> KIMI_API_KEY, base_url=https://api.kimi.com/coding/v1 +# Kimi Open Platform -> MOONSHOT_API_KEY, base_url=https://api.moonshot.ai/v1 +# StepFun -> STEPFUN_API_KEY +# 通用(任意 OpenAI 兼容接口)-> LLM_API_KEY +DEEPSEEK_API_KEY= +KIMI_API_KEY= +MOONSHOT_API_KEY= +STEPFUN_API_KEY= +OPENAI_API_KEY= +LLM_API_KEY= +GEMINI_API_KEY= +GOOGLE_API_KEY= + +# Optional LLM base URLs +# DEEPSEEK_BASE_URL=https://api.deepseek.com +# STEPFUN_BASE_URL=https://api.stepfun.com/v1 +# OPENAI_BASE_URL=https://api.openai.com/v1 + +# ================================================== +# Data source keys (optional) +# ================================================== +# TUSHARE_KEY= +# BOCHA_KEY= +# SERP_KEY= +# FMP_KEY= +# FINNHUB_KEY= + +# Market: CN-Stock | US-Stock +# CONTEST_TRADE_MARKET=CN-Stock diff --git a/config.yaml b/config.yaml index 7dead8f..fc0428a 100644 --- a/config.yaml +++ b/config.yaml @@ -72,15 +72,15 @@ finnhub_key: "" llm: provider: "openai" - base_url: "https://api.kimi.com/coding/v1" + base_url: "https://api.deepseek.com" api_key: null - model_name: "kimi-k3" + model_name: "deepseek-chat" llm_thinking: provider: "openai" - base_url: "https://api.kimi.com/coding/v1" + base_url: "https://api.deepseek.com" api_key: null - model_name: "kimi-k3" + model_name: "deepseek-reasoner" vlm: provider: "openai" @@ -105,7 +105,7 @@ data_agents_config: research_agent_config: belief_list_path: "config/belief_list.json" - max_react_step: 10 + max_react_step: 5 output_language: "中文" tools: - "tools.stock_symbol_search_akshare.stock_symbol_search" diff --git a/contest_trade/data_source/sina_news_crawl.py b/contest_trade/data_source/sina_news_crawl.py index ffdd034..47d9356 100644 --- a/contest_trade/data_source/sina_news_crawl.py +++ b/contest_trade/data_source/sina_news_crawl.py @@ -21,7 +21,7 @@ class SinaNewsCrawl(DataSourceBase): - def __init__(self, start_page=1, end_page=50): + def __init__(self, start_page=1, end_page=20): super().__init__("sina_news_crawl") self.start_page = start_page self.end_page = end_page @@ -35,7 +35,7 @@ def __init__(self, start_page=1, end_page=50): } self.all_items = [] self.fetch_full_intro = True # 是否抓取文章页以补全 intro - self.article_concurrency = 2 # 控制抓取文章页的并发 + self.article_concurrency = 10 # 控制抓取文章页的并发 async def fetch_page(self, session, page): """异步获取单个页面的数据""" diff --git a/contest_trade/models/llm_model.py b/contest_trade/models/llm_model.py index ec198d6..e368f12 100644 --- a/contest_trade/models/llm_model.py +++ b/contest_trade/models/llm_model.py @@ -55,7 +55,7 @@ def _normalize_provider(provider: str) -> str: class LLMModelConfig: def __init__(self, provider: str, model_name: str, api_key: str = None, base_url: str = None, - max_retries: int = 3, retry_delay: float = 20.0, timeout: float = 60.0, + max_retries: int = 3, retry_delay: float = 3.0, timeout: float = 60.0, extra_headers: dict = None, proxys: dict = None, **kwargs): self.provider = provider self.model_name = model_name diff --git a/contest_trade/utils/cache/market_manager/trade_calendar.json b/contest_trade/utils/cache/market_manager/trade_calendar.json index 93d754e..09ee86d 100644 --- a/contest_trade/utils/cache/market_manager/trade_calendar.json +++ b/contest_trade/utils/cache/market_manager/trade_calendar.json @@ -1,493 +1 @@ -{ - "description": "A股交易日历缓存文件(简化版,所有市场共用)", - "generated_by": "AKShare ak.tool_trade_date_hist_sina()", - "last_updated": "2025-08-20", - "date_range": "20240102 ~ 20251231", - "trade_dates": [ - "20240102", - "20240103", - "20240104", - "20240105", - "20240108", - "20240109", - "20240110", - "20240111", - "20240112", - "20240115", - "20240116", - "20240117", - "20240118", - "20240119", - "20240122", - "20240123", - "20240124", - "20240125", - "20240126", - "20240129", - "20240130", - "20240131", - "20240201", - "20240202", - "20240205", - "20240206", - "20240207", - "20240208", - "20240219", - "20240220", - "20240221", - "20240222", - "20240223", - "20240226", - "20240227", - "20240228", - "20240229", - "20240301", - "20240304", - "20240305", - "20240306", - "20240307", - "20240308", - "20240311", - "20240312", - "20240313", - "20240314", - "20240315", - "20240318", - "20240319", - "20240320", - "20240321", - "20240322", - "20240325", - "20240326", - "20240327", - "20240328", - "20240329", - "20240401", - "20240402", - "20240403", - "20240408", - "20240409", - "20240410", - "20240411", - "20240412", - "20240415", - "20240416", - "20240417", - "20240418", - "20240419", - "20240422", - "20240423", - "20240424", - "20240425", - "20240426", - "20240429", - "20240430", - "20240506", - "20240507", - "20240508", - "20240509", - "20240510", - "20240513", - "20240514", - "20240515", - "20240516", - "20240517", - "20240520", - "20240521", - "20240522", - "20240523", - "20240524", - "20240527", - "20240528", - "20240529", - "20240530", - "20240531", - "20240603", - "20240604", - "20240605", - "20240606", - "20240607", - "20240611", - "20240612", - "20240613", - "20240614", - "20240617", - "20240618", - "20240619", - "20240620", - "20240621", - "20240624", - "20240625", - "20240626", - "20240627", - "20240628", - "20240701", - "20240702", - "20240703", - "20240704", - "20240705", - "20240708", - "20240709", - "20240710", - "20240711", - "20240712", - "20240715", - "20240716", - "20240717", - "20240718", - "20240719", - "20240722", - "20240723", - "20240724", - "20240725", - "20240726", - "20240729", - "20240730", - "20240731", - "20240801", - "20240802", - "20240805", - "20240806", - "20240807", - "20240808", - "20240809", - "20240812", - "20240813", - "20240814", - "20240815", - "20240816", - "20240819", - "20240820", - "20240821", - "20240822", - "20240823", - "20240826", - "20240827", - "20240828", - "20240829", - "20240830", - "20240902", - "20240903", - "20240904", - "20240905", - "20240906", - "20240909", - "20240910", - "20240911", - "20240912", - "20240913", - "20240918", - "20240919", - "20240920", - "20240923", - "20240924", - "20240925", - "20240926", - "20240927", - "20240930", - "20241008", - "20241009", - "20241010", - "20241011", - "20241014", - "20241015", - "20241016", - "20241017", - "20241018", - "20241021", - "20241022", - "20241023", - "20241024", - "20241025", - "20241028", - "20241029", - "20241030", - "20241031", - "20241101", - "20241104", - "20241105", - "20241106", - "20241107", - "20241108", - "20241111", - "20241112", - "20241113", - "20241114", - "20241115", - "20241118", - "20241119", - "20241120", - "20241121", - "20241122", - "20241125", - "20241126", - "20241127", - "20241128", - "20241129", - "20241202", - "20241203", - "20241204", - "20241205", - "20241206", - "20241209", - "20241210", - "20241211", - "20241212", - "20241213", - "20241216", - "20241217", - "20241218", - "20241219", - "20241220", - "20241223", - "20241224", - "20241225", - "20241226", - "20241227", - "20241230", - "20241231", - "20250102", - "20250103", - "20250106", - "20250107", - "20250108", - "20250109", - "20250110", - "20250113", - "20250114", - "20250115", - "20250116", - "20250117", - "20250120", - "20250121", - "20250122", - "20250123", - "20250124", - "20250127", - "20250205", - "20250206", - "20250207", - "20250210", - "20250211", - "20250212", - "20250213", - "20250214", - "20250217", - "20250218", - "20250219", - "20250220", - "20250221", - "20250224", - "20250225", - "20250226", - "20250227", - "20250228", - "20250303", - "20250304", - "20250305", - "20250306", - "20250307", - "20250310", - "20250311", - "20250312", - "20250313", - "20250314", - "20250317", - "20250318", - "20250319", - "20250320", - "20250321", - "20250324", - "20250325", - "20250326", - "20250327", - "20250328", - "20250331", - "20250401", - "20250402", - "20250403", - "20250407", - "20250408", - "20250409", - "20250410", - "20250411", - "20250414", - "20250415", - "20250416", - "20250417", - "20250418", - "20250421", - "20250422", - "20250423", - "20250424", - "20250425", - "20250428", - "20250429", - "20250430", - "20250506", - "20250507", - "20250508", - "20250509", - "20250512", - "20250513", - "20250514", - "20250515", - "20250516", - "20250519", - "20250520", - "20250521", - "20250522", - "20250523", - "20250526", - "20250527", - "20250528", - "20250529", - "20250530", - "20250603", - "20250604", - "20250605", - "20250606", - "20250609", - "20250610", - "20250611", - "20250612", - "20250613", - "20250616", - "20250617", - "20250618", - "20250619", - "20250620", - "20250623", - "20250624", - "20250625", - "20250626", - "20250627", - "20250630", - "20250701", - "20250702", - "20250703", - "20250704", - "20250707", - "20250708", - "20250709", - "20250710", - "20250711", - "20250714", - "20250715", - "20250716", - "20250717", - "20250718", - "20250721", - "20250722", - "20250723", - "20250724", - "20250725", - "20250728", - "20250729", - "20250730", - "20250731", - "20250801", - "20250804", - "20250805", - "20250806", - "20250807", - "20250808", - "20250811", - "20250812", - "20250813", - "20250814", - "20250815", - "20250818", - "20250819", - "20250820", - "20250821", - "20250822", - "20250825", - "20250826", - "20250827", - "20250828", - "20250829", - "20250901", - "20250902", - "20250903", - "20250904", - "20250905", - "20250908", - "20250909", - "20250910", - "20250911", - "20250912", - "20250915", - "20250916", - "20250917", - "20250918", - "20250919", - "20250922", - "20250923", - "20250924", - "20250925", - "20250926", - "20250929", - "20250930", - "20251009", - "20251010", - "20251013", - "20251014", - "20251015", - "20251016", - "20251017", - "20251020", - "20251021", - "20251022", - "20251023", - "20251024", - "20251027", - "20251028", - "20251029", - "20251030", - "20251031", - "20251103", - "20251104", - "20251105", - "20251106", - "20251107", - "20251110", - "20251111", - "20251112", - "20251113", - "20251114", - "20251117", - "20251118", - "20251119", - "20251120", - "20251121", - "20251124", - "20251125", - "20251126", - "20251127", - "20251128", - "20251201", - "20251202", - "20251203", - "20251204", - "20251205", - "20251208", - "20251209", - "20251210", - "20251211", - "20251212", - "20251215", - "20251216", - "20251217", - "20251218", - "20251219", - "20251222", - "20251223", - "20251224", - "20251225", - "20251226", - "20251229", - "20251230", - "20251231" - ] -} \ No newline at end of file +{"trade_dates": ["20240102", "20240103", "20240104", "20240105", "20240108", "20240109", "20240110", "20240111", "20240112", "20240115", "20240116", "20240117", "20240118", "20240119", "20240122", "20240123", "20240124", "20240125", "20240126", "20240129", "20240130", "20240131", "20240201", "20240202", "20240205", "20240206", "20240207", "20240208", "20240219", "20240220", "20240221", "20240222", "20240223", "20240226", "20240227", "20240228", "20240229", "20240301", "20240304", "20240305", "20240306", "20240307", "20240308", "20240311", "20240312", "20240313", "20240314", "20240315", "20240318", "20240319", "20240320", "20240321", "20240322", "20240325", "20240326", "20240327", "20240328", "20240329", "20240401", "20240402", "20240403", "20240408", "20240409", "20240410", "20240411", "20240412", "20240415", "20240416", "20240417", "20240418", "20240419", "20240422", "20240423", "20240424", "20240425", "20240426", "20240429", "20240430", "20240506", "20240507", "20240508", "20240509", "20240510", "20240513", "20240514", "20240515", "20240516", "20240517", "20240520", "20240521", "20240522", "20240523", "20240524", "20240527", "20240528", "20240529", "20240530", "20240531", "20240603", "20240604", "20240605", "20240606", "20240607", "20240611", "20240612", "20240613", "20240614", "20240617", "20240618", "20240619", "20240620", "20240621", "20240624", "20240625", "20240626", "20240627", "20240628", "20240701", "20240702", "20240703", "20240704", "20240705", "20240708", "20240709", "20240710", "20240711", "20240712", "20240715", "20240716", "20240717", "20240718", "20240719", "20240722", "20240723", "20240724", "20240725", "20240726", "20240729", "20240730", "20240731", "20240801", "20240802", "20240805", "20240806", "20240807", "20240808", "20240809", "20240812", "20240813", "20240814", "20240815", "20240816", "20240819", "20240820", "20240821", "20240822", "20240823", "20240826", "20240827", "20240828", "20240829", "20240830", "20240902", "20240903", "20240904", "20240905", "20240906", "20240909", "20240910", "20240911", "20240912", "20240913", "20240918", "20240919", "20240920", "20240923", "20240924", "20240925", "20240926", "20240927", "20240930", "20241008", "20241009", "20241010", "20241011", "20241014", "20241015", "20241016", "20241017", "20241018", "20241021", "20241022", "20241023", "20241024", "20241025", "20241028", "20241029", "20241030", "20241031", "20241101", "20241104", "20241105", "20241106", "20241107", "20241108", "20241111", "20241112", "20241113", "20241114", "20241115", "20241118", "20241119", "20241120", "20241121", "20241122", "20241125", "20241126", "20241127", "20241128", "20241129", "20241202", "20241203", "20241204", "20241205", "20241206", "20241209", "20241210", "20241211", "20241212", "20241213", "20241216", "20241217", "20241218", "20241219", "20241220", "20241223", "20241224", "20241225", "20241226", "20241227", "20241230", "20241231", "20250102", "20250103", "20250106", "20250107", "20250108", "20250109", "20250110", "20250113", "20250114", "20250115", "20250116", "20250117", "20250120", "20250121", "20250122", "20250123", "20250124", "20250127", "20250205", "20250206", "20250207", "20250210", "20250211", "20250212", "20250213", "20250214", "20250217", "20250218", "20250219", "20250220", "20250221", "20250224", "20250225", "20250226", "20250227", "20250228", "20250303", "20250304", "20250305", "20250306", "20250307", "20250310", "20250311", "20250312", "20250313", "20250314", "20250317", "20250318", "20250319", "20250320", "20250321", "20250324", "20250325", "20250326", "20250327", "20250328", "20250331", "20250401", "20250402", "20250403", "20250407", "20250408", "20250409", "20250410", "20250411", "20250414", "20250415", "20250416", "20250417", "20250418", "20250421", "20250422", "20250423", "20250424", "20250425", "20250428", "20250429", "20250430", "20250506", "20250507", "20250508", "20250509", "20250512", "20250513", "20250514", "20250515", "20250516", "20250519", "20250520", "20250521", "20250522", "20250523", "20250526", "20250527", "20250528", "20250529", "20250530", "20250603", "20250604", "20250605", "20250606", "20250609", "20250610", "20250611", "20250612", "20250613", "20250616", "20250617", "20250618", "20250619", "20250620", "20250623", "20250624", "20250625", "20250626", "20250627", "20250630", "20250701", "20250702", "20250703", "20250704", "20250707", "20250708", "20250709", "20250710", "20250711", "20250714", "20250715", "20250716", "20250717", "20250718", "20250721", "20250722", "20250723", "20250724", "20250725", "20250728", "20250729", "20250730", "20250731", "20250801", "20250804", "20250805", "20250806", "20250807", "20250808", "20250811", "20250812", "20250813", "20250814", "20250815", "20250818", "20250819", "20250820", "20250821", "20250822", "20250825", "20250826", "20250827", "20250828", "20250829", "20250901", "20250902", "20250903", "20250904", "20250905", "20250908", "20250909", "20250910", "20250911", "20250912", "20250915", "20250916", "20250917", "20250918", "20250919", "20250922", "20250923", "20250924", "20250925", "20250926", "20250929", "20250930", "20251009", "20251010", "20251013", "20251014", "20251015", "20251016", "20251017", "20251020", "20251021", "20251022", "20251023", "20251024", "20251027", "20251028", "20251029", "20251030", "20251031", "20251103", "20251104", "20251105", "20251106", "20251107", "20251110", "20251111", "20251112", "20251113", "20251114", "20251117", "20251118", "20251119", "20251120", "20251121", "20251124", "20251125", "20251126", "20251127", "20251128", "20251201", "20251202", "20251203", "20251204", "20251205", "20251208", "20251209", "20251210", "20251211", "20251212", "20251215", "20251216", "20251217", "20251218", "20251219", "20251222", "20251223", "20251224", "20251225", "20251226", "20251229", "20251230", "20251231", "20260105", "20260106", "20260107", "20260108", "20260109", "20260112", "20260113", "20260114", "20260115", "20260116", "20260119", "20260120", "20260121", "20260122", "20260123", "20260126", "20260127", "20260128", "20260129", "20260130", "20260202", "20260203", "20260204", "20260205", "20260206", "20260209", "20260210", "20260211", "20260212", "20260213", "20260224", "20260225", "20260226", "20260227", "20260302", "20260303", "20260304", "20260305", "20260306", "20260309", "20260310", "20260311", "20260312", "20260313", "20260316", "20260317", "20260318", "20260319", "20260320", "20260323", "20260324", "20260325", "20260326", "20260327", "20260330", "20260331", "20260401", "20260402", "20260403", "20260407", "20260408", "20260409", "20260410", "20260413", "20260414", "20260415", "20260416", "20260417", "20260420", "20260421", "20260422", "20260423", "20260424", "20260427", "20260428", "20260429", "20260430", "20260506", "20260507", "20260508", "20260511", "20260512", "20260513", "20260514", "20260515", "20260518", "20260519", "20260520", "20260521", "20260522", "20260525", "20260526", "20260527", "20260528", "20260529", "20260601", "20260602", "20260603", "20260604", "20260605", "20260608", "20260609", "20260610", "20260611", "20260612", "20260615", "20260616", "20260617", "20260618", "20260622", "20260623", "20260624", "20260625", "20260626", "20260629", "20260630", "20260701", "20260702", "20260703", "20260706", "20260707", "20260708", "20260709", "20260710", "20260713", "20260714", "20260715", "20260716", "20260717", "20260720", "20260721", "20260722", "20260723", "20260724", "20260727", "20260728", "20260729", "20260730", "20260731", "20260803", "20260804", "20260805", "20260806", "20260807", "20260810", "20260811", "20260812", "20260813", "20260814", "20260817", "20260818", "20260819", "20260820", "20260821", "20260824", "20260825", "20260826", "20260827", "20260828", "20260831", "20260901", "20260902", "20260903", "20260904", "20260907", "20260908", "20260909", "20260910", "20260911", "20260914", "20260915", "20260916", "20260917", "20260918", "20260921", "20260922", "20260923", "20260924", "20260928", "20260929", "20260930", "20261008", "20261009", "20261012", "20261013", "20261014", "20261015", "20261016", "20261019", "20261020", "20261021", "20261022", "20261023", "20261026", "20261027", "20261028", "20261029", "20261030", "20261102", "20261103", "20261104", "20261105", "20261106", "20261109", "20261110", "20261111", "20261112", "20261113", "20261116", "20261117", "20261118", "20261119", "20261120", "20261123", "20261124", "20261125", "20261126", "20261127", "20261130", "20261201", "20261202", "20261203", "20261204", "20261207", "20261208", "20261209", "20261210", "20261211", "20261214", "20261215", "20261216", "20261217", "20261218", "20261221", "20261222", "20261223", "20261224", "20261225", "20261228", "20261229", "20261230", "20261231"]} \ No newline at end of file diff --git a/contest_trade/utils/market_manager.py b/contest_trade/utils/market_manager.py index 92669f8..0340add 100644 --- a/contest_trade/utils/market_manager.py +++ b/contest_trade/utils/market_manager.py @@ -462,9 +462,30 @@ def get_market_symbols(self, market: Union[Market, str], trigger_time: str, full raise ValueError(f"Invalid market: {market}") return df + def _refresh_trade_calendar_cache(self, cache_file: Path, verbose: bool = False) -> list: + """通过AKShare刷新交易日历缓存文件,返回交易日列表""" + import akshare as ak + if verbose: + print("交易日历缓存过期,正在通过AKShare刷新...") + trade_cal = ak.tool_trade_date_hist_sina() + trade_dates = [] + for date in trade_cal['trade_date']: + if hasattr(date, 'strftime'): + date_str = date.strftime('%Y%m%d') + else: + date_str = str(date).replace('-', '') + if date_str >= '20240101': + trade_dates.append(date_str) + trade_dates = sorted(set(trade_dates)) + with open(cache_file, 'w', encoding='utf-8') as f: + json.dump({"trade_dates": trade_dates}, f) + if verbose: + print(f"交易日历缓存刷新成功: {len(trade_dates)}个交易日,最新: {trade_dates[-1]}") + return trade_dates + def get_trade_date(self, market_name: str="CN-Stock", verbose: bool = False): """获取交易日历,优先级:缓存文件 -> AKShare -> Tushare""" - + # 方法1:尝试从缓存文件读取(A股相关市场) if market_name in ["CN-Stock", "CN-ETF", "CSI300", "CSI500", "CSI1000"]: try: @@ -472,44 +493,42 @@ def get_trade_date(self, market_name: str="CN-Stock", verbose: bool = False): if cache_file.exists(): with open(cache_file, 'r', encoding='utf-8') as f: trade_calendar_data = json.load(f) - - # 简化版:所有A股相关市场都使用同一个交易日历 + trade_dates = trade_calendar_data.get("trade_dates", []) - + + # 缓存最新日期距今超过30天则自动刷新 + if trade_dates: + from datetime import date + latest = trade_dates[-1] # 格式 YYYYMMDD + latest_date = date(int(latest[:4]), int(latest[4:6]), int(latest[6:])) + days_stale = (date.today() - latest_date).days + if days_stale > 30: + if verbose: + print(f"缓存最新交易日 {latest} 已过期 {days_stale} 天,刷新中...") + try: + trade_dates = self._refresh_trade_calendar_cache(cache_file, verbose) + except Exception as e: + if verbose: + print(f"缓存刷新失败,使用旧缓存: {e}") + else: + if verbose: + print(f"从缓存文件获取{market_name}交易日历成功: {len(trade_dates)}个交易日") + if trade_dates: - if verbose: - print(f"从缓存文件获取{market_name}交易日历成功: {len(trade_dates)}个交易日") return trade_dates except Exception as e: if verbose: print(f"缓存文件读取失败: {e}") - + # 方法2:尝试使用AKShare if market_name in ["CN-Stock", "CN-ETF", "CSI300", "CSI500", "CSI1000"]: try: - import akshare as ak - if verbose: - print(f"使用AKShare获取{market_name}交易日历...") - - trade_cal = ak.tool_trade_date_hist_sina() - trade_dates = [] - - for date in trade_cal['trade_date']: - if hasattr(date, 'strftime'): - date_str = date.strftime('%Y%m%d') - else: - date_str = str(date).replace('-', '') - - # 只保留2024年以后的数据,不限制结束时间 - if date_str >= '20240101': - trade_dates.append(date_str) - - trade_dates = sorted(list(set(trade_dates))) + cache_file = Path(__file__).parent / "cache" / "market_manager" / "trade_calendar.json" + trade_dates = self._refresh_trade_calendar_cache(cache_file, verbose) if trade_dates: if verbose: print(f"AKShare获取{market_name}交易日历成功: {len(trade_dates)}个交易日") return trade_dates - except Exception as e: if verbose: print(f"AKShare获取交易日历失败: {e}")