Backup before system reinstall
This commit is contained in:
1
jixia_academy/core/debate_system/engines/__init__.py
Normal file
1
jixia_academy/core/debate_system/engines/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# 稷下学宫引擎模块
|
||||
@@ -0,0 +1,43 @@
|
||||
# 设计八仙与数据源的智能映射
|
||||
immortal_data_mapping = {
|
||||
'吕洞宾': {
|
||||
'specialty': 'technical_analysis', # 技术分析专家
|
||||
'preferred_data_types': ['historical', 'price'],
|
||||
'data_providers': ['OpenBB', 'RapidAPI']
|
||||
},
|
||||
'何仙姑': {
|
||||
'specialty': 'risk_metrics', # 风险控制专家
|
||||
'preferred_data_types': ['price', 'profile'],
|
||||
'data_providers': ['RapidAPI', 'OpenBB']
|
||||
},
|
||||
'张果老': {
|
||||
'specialty': 'historical_data', # 历史数据分析师
|
||||
'preferred_data_types': ['historical'],
|
||||
'data_providers': ['OpenBB', 'RapidAPI']
|
||||
},
|
||||
'韩湘子': {
|
||||
'specialty': 'sector_analysis', # 新兴资产专家
|
||||
'preferred_data_types': ['profile', 'news'],
|
||||
'data_providers': ['RapidAPI', 'OpenBB']
|
||||
},
|
||||
'汉钟离': {
|
||||
'specialty': 'market_movers', # 热点追踪
|
||||
'preferred_data_types': ['news', 'price'],
|
||||
'data_providers': ['RapidAPI', 'OpenBB']
|
||||
},
|
||||
'蓝采和': {
|
||||
'specialty': 'value_discovery', # 潜力股发现
|
||||
'preferred_data_types': ['screener', 'profile'],
|
||||
'data_providers': ['OpenBB', 'RapidAPI']
|
||||
},
|
||||
'铁拐李': {
|
||||
'specialty': 'contrarian_analysis', # 逆向思维专家
|
||||
'preferred_data_types': ['profile', 'short_interest'],
|
||||
'data_providers': ['RapidAPI', 'OpenBB']
|
||||
},
|
||||
'曹国舅': {
|
||||
'specialty': 'macro_economics', # 宏观经济分析师
|
||||
'preferred_data_types': ['profile', 'institutional_holdings'],
|
||||
'data_providers': ['OpenBB', 'RapidAPI']
|
||||
}
|
||||
}
|
||||
38
jixia_academy/core/debate_system/engines/data_abstraction.py
Normal file
38
jixia_academy/core/debate_system/engines/data_abstraction.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
from src.jixia.models.financial_data_models import StockQuote, HistoricalPrice, CompanyProfile, FinancialNews
|
||||
|
||||
class DataProvider(ABC):
|
||||
"""金融数据提供商抽象基类"""
|
||||
|
||||
@abstractmethod
|
||||
def get_quote(self, symbol: str) -> Optional[StockQuote]:
|
||||
"""获取股票报价"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_historical_prices(self, symbol: str, days: int = 30) -> List[HistoricalPrice]:
|
||||
"""获取历史价格数据"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_company_profile(self, symbol: str) -> Optional[CompanyProfile]:
|
||||
"""获取公司概况"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_news(self, symbol: str, limit: int = 10) -> List[FinancialNews]:
|
||||
"""获取相关新闻"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""数据提供商名称"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def priority(self) -> int:
|
||||
"""优先级(数字越小优先级越高)"""
|
||||
pass
|
||||
@@ -0,0 +1,109 @@
|
||||
from typing import List, Optional
|
||||
import asyncio
|
||||
from src.jixia.engines.data_abstraction import DataProvider
|
||||
from src.jixia.models.financial_data_models import StockQuote, HistoricalPrice, CompanyProfile, FinancialNews
|
||||
from src.jixia.engines.rapidapi_adapter import RapidAPIDataProvider
|
||||
from src.jixia.engines.openbb_adapter import OpenBBDataProvider
|
||||
|
||||
class DataAbstractionLayer:
|
||||
"""金融数据抽象层管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.providers: List[DataProvider] = []
|
||||
self._initialize_providers()
|
||||
|
||||
def _initialize_providers(self):
|
||||
"""初始化所有可用的数据提供商"""
|
||||
# 根据配置和环境动态加载适配器
|
||||
try:
|
||||
self.providers.append(OpenBBDataProvider())
|
||||
except Exception as e:
|
||||
print(f"警告: OpenBBDataProvider 初始化失败: {e}")
|
||||
|
||||
try:
|
||||
self.providers.append(RapidAPIDataProvider())
|
||||
except Exception as e:
|
||||
print(f"警告: RapidAPIDataProvider 初始化失败: {e}")
|
||||
|
||||
# 按优先级排序
|
||||
self.providers.sort(key=lambda p: p.priority)
|
||||
print(f"数据抽象层初始化完成,已加载 {len(self.providers)} 个数据提供商")
|
||||
for provider in self.providers:
|
||||
print(f" - {provider.name} (优先级: {provider.priority})")
|
||||
|
||||
def get_quote(self, symbol: str) -> Optional[StockQuote]:
|
||||
"""获取股票报价(带故障转移)"""
|
||||
for provider in self.providers:
|
||||
try:
|
||||
quote = provider.get_quote(symbol)
|
||||
if quote:
|
||||
print(f"✅ 通过 {provider.name} 获取到 {symbol} 的报价")
|
||||
return quote
|
||||
except Exception as e:
|
||||
print(f"警告: {provider.name} 获取报价失败: {e}")
|
||||
continue
|
||||
print(f"❌ 所有数据提供商都无法获取 {symbol} 的报价")
|
||||
return None
|
||||
|
||||
async def get_quote_async(self, symbol: str) -> Optional[StockQuote]:
|
||||
"""异步获取股票报价(带故障转移)"""
|
||||
for provider in self.providers:
|
||||
try:
|
||||
# 如果提供商支持异步方法,则使用异步方法
|
||||
if hasattr(provider, 'get_quote_async'):
|
||||
quote = await provider.get_quote_async(symbol)
|
||||
else:
|
||||
# 否则在执行器中运行同步方法
|
||||
quote = await asyncio.get_event_loop().run_in_executor(
|
||||
None, provider.get_quote, symbol
|
||||
)
|
||||
if quote:
|
||||
print(f"✅ 通过 {provider.name} 异步获取到 {symbol} 的报价")
|
||||
return quote
|
||||
except Exception as e:
|
||||
print(f"警告: {provider.name} 异步获取报价失败: {e}")
|
||||
continue
|
||||
print(f"❌ 所有数据提供商都无法异步获取 {symbol} 的报价")
|
||||
return None
|
||||
|
||||
def get_historical_prices(self, symbol: str, days: int = 30) -> List[HistoricalPrice]:
|
||||
"""获取历史价格数据(带故障转移)"""
|
||||
for provider in self.providers:
|
||||
try:
|
||||
prices = provider.get_historical_prices(symbol, days)
|
||||
if prices:
|
||||
print(f"✅ 通过 {provider.name} 获取到 {symbol} 的历史价格数据")
|
||||
return prices
|
||||
except Exception as e:
|
||||
print(f"警告: {provider.name} 获取历史价格失败: {e}")
|
||||
continue
|
||||
print(f"❌ 所有数据提供商都无法获取 {symbol} 的历史价格数据")
|
||||
return []
|
||||
|
||||
def get_company_profile(self, symbol: str) -> Optional[CompanyProfile]:
|
||||
"""获取公司概况(带故障转移)"""
|
||||
for provider in self.providers:
|
||||
try:
|
||||
profile = provider.get_company_profile(symbol)
|
||||
if profile:
|
||||
print(f"✅ 通过 {provider.name} 获取到 {symbol} 的公司概况")
|
||||
return profile
|
||||
except Exception as e:
|
||||
print(f"警告: {provider.name} 获取公司概况失败: {e}")
|
||||
continue
|
||||
print(f"❌ 所有数据提供商都无法获取 {symbol} 的公司概况")
|
||||
return None
|
||||
|
||||
def get_news(self, symbol: str, limit: int = 10) -> List[FinancialNews]:
|
||||
"""获取相关新闻(带故障转移)"""
|
||||
for provider in self.providers:
|
||||
try:
|
||||
news = provider.get_news(symbol, limit)
|
||||
if news:
|
||||
print(f"✅ 通过 {provider.name} 获取到 {symbol} 的相关新闻")
|
||||
return news
|
||||
except Exception as e:
|
||||
print(f"警告: {provider.name} 获取新闻失败: {e}")
|
||||
continue
|
||||
print(f"❌ 所有数据提供商都无法获取 {symbol} 的相关新闻")
|
||||
return []
|
||||
37
jixia_academy/core/debate_system/engines/data_cache.py
Normal file
37
jixia_academy/core/debate_system/engines/data_cache.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
from functools import lru_cache
|
||||
|
||||
class DataCache:
|
||||
"""金融数据缓存"""
|
||||
|
||||
def __init__(self):
|
||||
self._cache = {}
|
||||
self._cache_times = {}
|
||||
self.default_ttl = 60 # 默认缓存时间(秒)
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
"""获取缓存数据"""
|
||||
if key in self._cache:
|
||||
# 检查是否过期
|
||||
if time.time() - self._cache_times[key] < self.default_ttl:
|
||||
return self._cache[key]
|
||||
else:
|
||||
# 删除过期缓存
|
||||
del self._cache[key]
|
||||
del self._cache_times[key]
|
||||
return None
|
||||
|
||||
def set(self, key: str, value: Any, ttl: Optional[int] = None):
|
||||
"""设置缓存数据"""
|
||||
self._cache[key] = value
|
||||
self._cache_times[key] = time.time()
|
||||
if ttl:
|
||||
# 可以为特定数据设置不同的TTL
|
||||
pass # 实际实现中需要更复杂的TTL管理机制
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_quote_cache(self, symbol: str) -> Optional[Any]:
|
||||
"""LRU缓存装饰器示例"""
|
||||
# 这个方法将自动缓存最近128个调用的结果
|
||||
pass
|
||||
@@ -0,0 +1,49 @@
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
class DataQualityMonitor:
|
||||
"""数据质量监控"""
|
||||
|
||||
def __init__(self):
|
||||
self.provider_stats = {}
|
||||
|
||||
def record_access(self, provider_name: str, success: bool, response_time: float, data_size: int):
|
||||
"""记录数据访问统计"""
|
||||
if provider_name not in self.provider_stats:
|
||||
self.provider_stats[provider_name] = {
|
||||
'total_requests': 0,
|
||||
'successful_requests': 0,
|
||||
'failed_requests': 0,
|
||||
'total_response_time': 0,
|
||||
'total_data_size': 0,
|
||||
'last_access': None
|
||||
}
|
||||
|
||||
stats = self.provider_stats[provider_name]
|
||||
stats['total_requests'] += 1
|
||||
if success:
|
||||
stats['successful_requests'] += 1
|
||||
else:
|
||||
stats['failed_requests'] += 1
|
||||
stats['total_response_time'] += response_time
|
||||
stats['total_data_size'] += data_size
|
||||
stats['last_access'] = datetime.now()
|
||||
|
||||
def get_provider_health(self, provider_name: str) -> Dict[str, Any]:
|
||||
"""获取提供商健康状况"""
|
||||
if provider_name not in self.provider_stats:
|
||||
return {'status': 'unknown'}
|
||||
|
||||
stats = self.provider_stats[provider_name]
|
||||
success_rate = stats['successful_requests'] / stats['total_requests'] if stats['total_requests'] > 0 else 0
|
||||
avg_response_time = stats['total_response_time'] / stats['total_requests'] if stats['total_requests'] > 0 else 0
|
||||
|
||||
status = 'healthy' if success_rate > 0.95 and avg_response_time < 2.0 else 'degraded' if success_rate > 0.8 else 'unhealthy'
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
'success_rate': success_rate,
|
||||
'avg_response_time': avg_response_time,
|
||||
'total_requests': stats['total_requests'],
|
||||
'last_access': stats['last_access']
|
||||
}
|
||||
462
jixia_academy/core/debate_system/engines/jixia_load_balancer.py
Normal file
462
jixia_academy/core/debate_system/engines/jixia_load_balancer.py
Normal file
@@ -0,0 +1,462 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
稷下学宫负载均衡器
|
||||
实现八仙论道的API负载分担策略
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import requests
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import os
|
||||
|
||||
@dataclass
|
||||
class APIResult:
|
||||
"""API调用结果"""
|
||||
success: bool
|
||||
data: Dict[str, Any]
|
||||
api_used: str
|
||||
response_time: float
|
||||
error: Optional[str] = None
|
||||
cached: bool = False
|
||||
|
||||
class RateLimiter:
|
||||
"""速率限制器"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_calls = defaultdict(list)
|
||||
self.limits = {
|
||||
'alpha_vantage': {'per_minute': 500, 'per_month': 500000},
|
||||
'yahoo_finance_15': {'per_minute': 500, 'per_month': 500000},
|
||||
'webull': {'per_minute': 500, 'per_month': 500000},
|
||||
'seeking_alpha': {'per_minute': 500, 'per_month': 500000}
|
||||
}
|
||||
|
||||
def is_rate_limited(self, api_name: str) -> bool:
|
||||
"""检查是否达到速率限制"""
|
||||
now = time.time()
|
||||
calls = self.api_calls[api_name]
|
||||
|
||||
# 清理1分钟前的记录
|
||||
self.api_calls[api_name] = [call_time for call_time in calls if now - call_time < 60]
|
||||
|
||||
# 检查每分钟限制
|
||||
if len(self.api_calls[api_name]) >= self.limits[api_name]['per_minute'] * 0.9: # 90%阈值
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def record_call(self, api_name: str):
|
||||
"""记录API调用"""
|
||||
self.api_calls[api_name].append(time.time())
|
||||
|
||||
class APIHealthChecker:
|
||||
"""API健康检查器"""
|
||||
|
||||
def __init__(self):
|
||||
self.health_status = {
|
||||
'alpha_vantage': {'healthy': True, 'last_check': 0, 'consecutive_failures': 0},
|
||||
'yahoo_finance_15': {'healthy': True, 'last_check': 0, 'consecutive_failures': 0},
|
||||
'webull': {'healthy': True, 'last_check': 0, 'consecutive_failures': 0},
|
||||
'seeking_alpha': {'healthy': True, 'last_check': 0, 'consecutive_failures': 0}
|
||||
}
|
||||
self.check_interval = 300 # 5分钟检查一次
|
||||
|
||||
def is_healthy(self, api_name: str) -> bool:
|
||||
"""检查API是否健康"""
|
||||
status = self.health_status[api_name]
|
||||
now = time.time()
|
||||
|
||||
# 如果距离上次检查超过间隔时间,进行健康检查
|
||||
if now - status['last_check'] > self.check_interval:
|
||||
self._perform_health_check(api_name)
|
||||
|
||||
return status['healthy']
|
||||
|
||||
def _perform_health_check(self, api_name: str):
|
||||
"""执行健康检查"""
|
||||
# 这里可以实现具体的健康检查逻辑
|
||||
# 暂时简化为基于连续失败次数判断
|
||||
status = self.health_status[api_name]
|
||||
status['last_check'] = time.time()
|
||||
|
||||
# 如果连续失败超过3次,标记为不健康
|
||||
if status['consecutive_failures'] > 3:
|
||||
status['healthy'] = False
|
||||
else:
|
||||
status['healthy'] = True
|
||||
|
||||
def record_success(self, api_name: str):
|
||||
"""记录成功调用"""
|
||||
self.health_status[api_name]['consecutive_failures'] = 0
|
||||
self.health_status[api_name]['healthy'] = True
|
||||
|
||||
def record_failure(self, api_name: str):
|
||||
"""记录失败调用"""
|
||||
self.health_status[api_name]['consecutive_failures'] += 1
|
||||
|
||||
class DataNormalizer:
|
||||
"""数据标准化处理器"""
|
||||
|
||||
def normalize_stock_quote(self, raw_data: dict, api_source: str) -> dict:
|
||||
"""将不同API的股票报价数据标准化"""
|
||||
try:
|
||||
if api_source == 'alpha_vantage':
|
||||
return self._normalize_alpha_vantage_quote(raw_data)
|
||||
elif api_source == 'yahoo_finance_15':
|
||||
return self._normalize_yahoo_quote(raw_data)
|
||||
elif api_source == 'webull':
|
||||
return self._normalize_webull_quote(raw_data)
|
||||
elif api_source == 'seeking_alpha':
|
||||
return self._normalize_seeking_alpha_quote(raw_data)
|
||||
else:
|
||||
return {'error': f'Unknown API source: {api_source}'}
|
||||
except Exception as e:
|
||||
return {'error': f'Data normalization failed: {str(e)}'}
|
||||
|
||||
def _normalize_alpha_vantage_quote(self, data: dict) -> dict:
|
||||
"""标准化Alpha Vantage数据格式"""
|
||||
global_quote = data.get('Global Quote', {})
|
||||
return {
|
||||
'symbol': global_quote.get('01. symbol'),
|
||||
'price': float(global_quote.get('05. price', 0)),
|
||||
'change': float(global_quote.get('09. change', 0)),
|
||||
'change_percent': global_quote.get('10. change percent', '0%'),
|
||||
'volume': int(global_quote.get('06. volume', 0)),
|
||||
'high': float(global_quote.get('03. high', 0)),
|
||||
'low': float(global_quote.get('04. low', 0)),
|
||||
'source': 'alpha_vantage',
|
||||
'timestamp': global_quote.get('07. latest trading day')
|
||||
}
|
||||
|
||||
def _normalize_yahoo_quote(self, data: dict) -> dict:
|
||||
"""标准化Yahoo Finance数据格式"""
|
||||
body = data.get('body', {})
|
||||
return {
|
||||
'symbol': body.get('symbol'),
|
||||
'price': float(body.get('regularMarketPrice', 0)),
|
||||
'change': float(body.get('regularMarketChange', 0)),
|
||||
'change_percent': f"{body.get('regularMarketChangePercent', 0):.2f}%",
|
||||
'volume': int(body.get('regularMarketVolume', 0)),
|
||||
'high': float(body.get('regularMarketDayHigh', 0)),
|
||||
'low': float(body.get('regularMarketDayLow', 0)),
|
||||
'source': 'yahoo_finance_15',
|
||||
'timestamp': body.get('regularMarketTime')
|
||||
}
|
||||
|
||||
def _normalize_webull_quote(self, data: dict) -> dict:
|
||||
"""标准化Webull数据格式"""
|
||||
if 'stocks' in data and len(data['stocks']) > 0:
|
||||
stock = data['stocks'][0]
|
||||
return {
|
||||
'symbol': stock.get('symbol'),
|
||||
'price': float(stock.get('close', 0)),
|
||||
'change': float(stock.get('change', 0)),
|
||||
'change_percent': f"{stock.get('changeRatio', 0):.2f}%",
|
||||
'volume': int(stock.get('volume', 0)),
|
||||
'high': float(stock.get('high', 0)),
|
||||
'low': float(stock.get('low', 0)),
|
||||
'source': 'webull',
|
||||
'timestamp': stock.get('timeStamp')
|
||||
}
|
||||
return {'error': 'No stock data found in Webull response'}
|
||||
|
||||
def _normalize_seeking_alpha_quote(self, data: dict) -> dict:
|
||||
"""标准化Seeking Alpha数据格式"""
|
||||
if 'data' in data and len(data['data']) > 0:
|
||||
stock_data = data['data'][0]
|
||||
attributes = stock_data.get('attributes', {})
|
||||
return {
|
||||
'symbol': attributes.get('slug'),
|
||||
'price': float(attributes.get('lastPrice', 0)),
|
||||
'change': float(attributes.get('dayChange', 0)),
|
||||
'change_percent': f"{attributes.get('dayChangePercent', 0):.2f}%",
|
||||
'volume': int(attributes.get('volume', 0)),
|
||||
'source': 'seeking_alpha',
|
||||
'market_cap': attributes.get('marketCap'),
|
||||
'pe_ratio': attributes.get('peRatio')
|
||||
}
|
||||
return {'error': 'No data found in Seeking Alpha response'}
|
||||
|
||||
class JixiaLoadBalancer:
|
||||
"""稷下学宫负载均衡器"""
|
||||
|
||||
def __init__(self, rapidapi_key: str):
|
||||
self.rapidapi_key = rapidapi_key
|
||||
self.rate_limiter = RateLimiter()
|
||||
self.health_checker = APIHealthChecker()
|
||||
self.data_normalizer = DataNormalizer()
|
||||
self.cache = {} # 简单的内存缓存
|
||||
self.cache_ttl = 300 # 5分钟缓存
|
||||
|
||||
# API配置
|
||||
self.api_configs = {
|
||||
'alpha_vantage': {
|
||||
'host': 'alpha-vantage.p.rapidapi.com',
|
||||
'endpoints': {
|
||||
'stock_quote': '/query?function=GLOBAL_QUOTE&symbol={symbol}',
|
||||
'company_overview': '/query?function=OVERVIEW&symbol={symbol}',
|
||||
'earnings': '/query?function=EARNINGS&symbol={symbol}'
|
||||
}
|
||||
},
|
||||
'yahoo_finance_15': {
|
||||
'host': 'yahoo-finance15.p.rapidapi.com',
|
||||
'endpoints': {
|
||||
'stock_quote': '/api/yahoo/qu/quote/{symbol}',
|
||||
'market_movers': '/api/yahoo/co/collections/day_gainers',
|
||||
'market_news': '/api/yahoo/ne/news'
|
||||
}
|
||||
},
|
||||
'webull': {
|
||||
'host': 'webull.p.rapidapi.com',
|
||||
'endpoints': {
|
||||
'stock_quote': '/stock/search?keyword={symbol}',
|
||||
'market_movers': '/market/get-active-gainers'
|
||||
}
|
||||
},
|
||||
'seeking_alpha': {
|
||||
'host': 'seeking-alpha.p.rapidapi.com',
|
||||
'endpoints': {
|
||||
'company_overview': '/symbols/get-profile?symbols={symbol}',
|
||||
'market_news': '/news/list?category=market-news'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 八仙API分配策略
|
||||
self.immortal_api_mapping = {
|
||||
'stock_quote': {
|
||||
'吕洞宾': 'alpha_vantage', # 主力剑仙用最快的API
|
||||
'何仙姑': 'yahoo_finance_15', # 风控专家用稳定的API
|
||||
'张果老': 'webull', # 技术分析师用搜索强的API
|
||||
'韩湘子': 'alpha_vantage', # 基本面研究用专业API
|
||||
'汉钟离': 'yahoo_finance_15', # 量化专家用市场数据API
|
||||
'蓝采和': 'webull', # 情绪分析师用活跃数据API
|
||||
'曹国舅': 'seeking_alpha', # 宏观分析师用分析API
|
||||
'铁拐李': 'alpha_vantage' # 逆向投资用基础数据API
|
||||
},
|
||||
'company_overview': {
|
||||
'吕洞宾': 'alpha_vantage',
|
||||
'何仙姑': 'seeking_alpha',
|
||||
'张果老': 'alpha_vantage',
|
||||
'韩湘子': 'seeking_alpha',
|
||||
'汉钟离': 'alpha_vantage',
|
||||
'蓝采和': 'seeking_alpha',
|
||||
'曹国舅': 'seeking_alpha',
|
||||
'铁拐李': 'alpha_vantage'
|
||||
},
|
||||
'market_movers': {
|
||||
'吕洞宾': 'yahoo_finance_15',
|
||||
'何仙姑': 'webull',
|
||||
'张果老': 'yahoo_finance_15',
|
||||
'韩湘子': 'webull',
|
||||
'汉钟离': 'yahoo_finance_15',
|
||||
'蓝采和': 'webull',
|
||||
'曹国舅': 'yahoo_finance_15',
|
||||
'铁拐李': 'webull'
|
||||
},
|
||||
'market_news': {
|
||||
'吕洞宾': 'yahoo_finance_15',
|
||||
'何仙姑': 'seeking_alpha',
|
||||
'张果老': 'yahoo_finance_15',
|
||||
'韩湘子': 'seeking_alpha',
|
||||
'汉钟离': 'yahoo_finance_15',
|
||||
'蓝采和': 'seeking_alpha',
|
||||
'曹国舅': 'seeking_alpha',
|
||||
'铁拐李': 'yahoo_finance_15'
|
||||
}
|
||||
}
|
||||
|
||||
# 故障转移优先级
|
||||
self.failover_priority = {
|
||||
'alpha_vantage': ['webull', 'yahoo_finance_15'],
|
||||
'yahoo_finance_15': ['webull', 'alpha_vantage'],
|
||||
'webull': ['alpha_vantage', 'yahoo_finance_15'],
|
||||
'seeking_alpha': ['yahoo_finance_15', 'alpha_vantage']
|
||||
}
|
||||
|
||||
def get_data_for_immortal(self, immortal_name: str, data_type: str, symbol: str = None) -> APIResult:
|
||||
"""为特定仙人获取数据"""
|
||||
print(f"🎭 {immortal_name} 正在获取 {data_type} 数据...")
|
||||
|
||||
# 检查缓存
|
||||
cache_key = f"{immortal_name}_{data_type}_{symbol}"
|
||||
cached_result = self._get_cached_data(cache_key)
|
||||
if cached_result:
|
||||
print(f" 📦 使用缓存数据")
|
||||
return cached_result
|
||||
|
||||
# 获取该仙人的首选API
|
||||
if data_type not in self.immortal_api_mapping:
|
||||
return APIResult(False, {}, '', 0, f"Unsupported data type: {data_type}")
|
||||
|
||||
preferred_api = self.immortal_api_mapping[data_type][immortal_name]
|
||||
|
||||
# 尝试首选API
|
||||
result = self._try_api(preferred_api, data_type, symbol)
|
||||
if result.success:
|
||||
self._cache_data(cache_key, result)
|
||||
print(f" ✅ 成功从 {preferred_api} 获取数据 (响应时间: {result.response_time:.2f}s)")
|
||||
return result
|
||||
|
||||
# 故障转移到备用API
|
||||
print(f" ⚠️ {preferred_api} 不可用,尝试备用API...")
|
||||
backup_apis = self.failover_priority.get(preferred_api, [])
|
||||
|
||||
for backup_api in backup_apis:
|
||||
if data_type in self.api_configs[backup_api]['endpoints']:
|
||||
result = self._try_api(backup_api, data_type, symbol)
|
||||
if result.success:
|
||||
self._cache_data(cache_key, result)
|
||||
print(f" ✅ 成功从备用API {backup_api} 获取数据 (响应时间: {result.response_time:.2f}s)")
|
||||
return result
|
||||
|
||||
# 所有API都失败
|
||||
print(f" ❌ 所有API都不可用")
|
||||
return APIResult(False, {}, '', 0, "All APIs failed")
|
||||
|
||||
def _try_api(self, api_name: str, data_type: str, symbol: str = None) -> APIResult:
|
||||
"""尝试调用指定API"""
|
||||
# 检查API健康状态和速率限制
|
||||
if not self.health_checker.is_healthy(api_name):
|
||||
return APIResult(False, {}, api_name, 0, "API is unhealthy")
|
||||
|
||||
if self.rate_limiter.is_rate_limited(api_name):
|
||||
return APIResult(False, {}, api_name, 0, "Rate limited")
|
||||
|
||||
# 构建请求
|
||||
config = self.api_configs[api_name]
|
||||
if data_type not in config['endpoints']:
|
||||
return APIResult(False, {}, api_name, 0, f"Endpoint {data_type} not supported")
|
||||
|
||||
endpoint = config['endpoints'][data_type]
|
||||
if symbol and '{symbol}' in endpoint:
|
||||
endpoint = endpoint.format(symbol=symbol)
|
||||
|
||||
url = f"https://{config['host']}{endpoint}"
|
||||
headers = {
|
||||
'X-RapidAPI-Key': self.rapidapi_key,
|
||||
'X-RapidAPI-Host': config['host']
|
||||
}
|
||||
|
||||
# 发起请求
|
||||
start_time = time.time()
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response_time = time.time() - start_time
|
||||
|
||||
self.rate_limiter.record_call(api_name)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# 数据标准化
|
||||
if data_type == 'stock_quote':
|
||||
normalized_data = self.data_normalizer.normalize_stock_quote(data, api_name)
|
||||
else:
|
||||
normalized_data = data
|
||||
|
||||
self.health_checker.record_success(api_name)
|
||||
return APIResult(True, normalized_data, api_name, response_time)
|
||||
else:
|
||||
error_msg = f"HTTP {response.status_code}: {response.text[:200]}"
|
||||
self.health_checker.record_failure(api_name)
|
||||
return APIResult(False, {}, api_name, response_time, error_msg)
|
||||
|
||||
except Exception as e:
|
||||
response_time = time.time() - start_time
|
||||
self.health_checker.record_failure(api_name)
|
||||
return APIResult(False, {}, api_name, response_time, str(e))
|
||||
|
||||
def _get_cached_data(self, cache_key: str) -> Optional[APIResult]:
|
||||
"""获取缓存数据"""
|
||||
if cache_key in self.cache:
|
||||
cached_item = self.cache[cache_key]
|
||||
if time.time() - cached_item['timestamp'] < self.cache_ttl:
|
||||
result = cached_item['result']
|
||||
result.cached = True
|
||||
return result
|
||||
else:
|
||||
# 缓存过期,删除
|
||||
del self.cache[cache_key]
|
||||
return None
|
||||
|
||||
def _cache_data(self, cache_key: str, result: APIResult):
|
||||
"""缓存数据"""
|
||||
self.cache[cache_key] = {
|
||||
'result': result,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
def get_load_distribution(self) -> dict:
|
||||
"""获取负载分布统计"""
|
||||
api_calls = {}
|
||||
total_calls = 0
|
||||
|
||||
for api_name, calls in self.rate_limiter.api_calls.items():
|
||||
call_count = len(calls)
|
||||
api_calls[api_name] = call_count
|
||||
total_calls += call_count
|
||||
|
||||
if total_calls == 0:
|
||||
return {}
|
||||
|
||||
distribution = {}
|
||||
for api_name, call_count in api_calls.items():
|
||||
health_status = self.health_checker.health_status[api_name]
|
||||
distribution[api_name] = {
|
||||
'calls': call_count,
|
||||
'percentage': (call_count / total_calls) * 100,
|
||||
'healthy': health_status['healthy'],
|
||||
'consecutive_failures': health_status['consecutive_failures']
|
||||
}
|
||||
|
||||
return distribution
|
||||
|
||||
def conduct_immortal_debate(self, topic_symbol: str) -> Dict[str, APIResult]:
|
||||
"""进行八仙论道,每个仙人获取不同的数据"""
|
||||
print(f"\n🏛️ 稷下学宫八仙论道开始 - 主题: {topic_symbol}")
|
||||
print("=" * 60)
|
||||
|
||||
immortals = ['吕洞宾', '何仙姑', '张果老', '韩湘子', '汉钟离', '蓝采和', '曹国舅', '铁拐李']
|
||||
debate_results = {}
|
||||
|
||||
# 每个仙人获取股票报价数据
|
||||
for immortal in immortals:
|
||||
result = self.get_data_for_immortal(immortal, 'stock_quote', topic_symbol)
|
||||
debate_results[immortal] = result
|
||||
|
||||
if result.success:
|
||||
data = result.data
|
||||
if 'price' in data:
|
||||
print(f" 💰 {immortal}: ${data['price']:.2f} ({data.get('change_percent', 'N/A')}) via {result.api_used}")
|
||||
|
||||
time.sleep(0.2) # 避免过快请求
|
||||
|
||||
print("\n📊 负载分布统计:")
|
||||
distribution = self.get_load_distribution()
|
||||
for api_name, stats in distribution.items():
|
||||
print(f" {api_name}: {stats['calls']} 次调用 ({stats['percentage']:.1f}%) - {'健康' if stats['healthy'] else '异常'}")
|
||||
|
||||
return debate_results
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 从环境变量获取API密钥
|
||||
rapidapi_key = os.getenv('RAPIDAPI_KEY')
|
||||
if not rapidapi_key:
|
||||
print("❌ 请设置RAPIDAPI_KEY环境变量")
|
||||
exit(1)
|
||||
|
||||
# 创建负载均衡器
|
||||
load_balancer = JixiaLoadBalancer(rapidapi_key)
|
||||
|
||||
# 进行八仙论道
|
||||
results = load_balancer.conduct_immortal_debate('TSLA')
|
||||
|
||||
print("\n🎉 八仙论道完成!")
|
||||
75
jixia_academy/core/debate_system/engines/openbb_adapter.py
Normal file
75
jixia_academy/core/debate_system/engines/openbb_adapter.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from typing import List, Optional
|
||||
from src.jixia.engines.data_abstraction import DataProvider
|
||||
from src.jixia.models.financial_data_models import StockQuote, HistoricalPrice, CompanyProfile, FinancialNews
|
||||
from src.jixia.engines.openbb_engine import OpenBBEngine
|
||||
|
||||
class OpenBBDataProvider(DataProvider):
|
||||
"""OpenBB引擎适配器"""
|
||||
|
||||
def __init__(self):
|
||||
self.engine = OpenBBEngine()
|
||||
self._name = "OpenBB"
|
||||
self._priority = 1 # 最高优先级
|
||||
|
||||
def get_quote(self, symbol: str) -> Optional[StockQuote]:
|
||||
result = self.engine.get_immortal_data("吕洞宾", "price", symbol)
|
||||
if result.success and result.data:
|
||||
# 解析OpenBB返回的数据并转换为StockQuote
|
||||
# 注意:这里需要根据OpenBB实际返回的数据结构进行调整
|
||||
data = result.data
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
item = data[0] # 取第一条数据
|
||||
elif hasattr(data, '__dict__'):
|
||||
item = data
|
||||
else:
|
||||
item = {}
|
||||
|
||||
# 提取价格信息(根据openbb_stock_data.py中的字段)
|
||||
price = 0
|
||||
if hasattr(item, 'close'):
|
||||
price = float(item.close)
|
||||
elif isinstance(item, dict) and 'close' in item:
|
||||
price = float(item['close'])
|
||||
|
||||
volume = 0
|
||||
if hasattr(item, 'volume'):
|
||||
volume = int(item.volume)
|
||||
elif isinstance(item, dict) and 'volume' in item:
|
||||
volume = int(item['volume'])
|
||||
|
||||
# 日期处理
|
||||
timestamp = None
|
||||
if hasattr(item, 'date'):
|
||||
timestamp = item.date
|
||||
elif isinstance(item, dict) and 'date' in item:
|
||||
timestamp = item['date']
|
||||
|
||||
return StockQuote(
|
||||
symbol=symbol,
|
||||
price=price,
|
||||
change=0, # 需要计算
|
||||
change_percent=0, # 需要计算
|
||||
volume=volume,
|
||||
timestamp=timestamp
|
||||
)
|
||||
return None
|
||||
|
||||
def get_historical_prices(self, symbol: str, days: int = 30) -> List[HistoricalPrice]:
|
||||
# 实现历史价格数据获取逻辑
|
||||
pass
|
||||
|
||||
def get_company_profile(self, symbol: str) -> Optional[CompanyProfile]:
|
||||
# 实现公司概况获取逻辑
|
||||
pass
|
||||
|
||||
def get_news(self, symbol: str, limit: int = 10) -> List[FinancialNews]:
|
||||
# 实现新闻获取逻辑
|
||||
pass
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return self._priority
|
||||
225
jixia_academy/core/debate_system/engines/openbb_engine.py
Normal file
225
jixia_academy/core/debate_system/engines/openbb_engine.py
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenBB 集成引擎
|
||||
为八仙论道提供更丰富的金融数据支撑
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImmortalConfig:
|
||||
"""八仙配置数据类"""
|
||||
primary: str
|
||||
specialty: str
|
||||
|
||||
@dataclass
|
||||
class APIResult:
|
||||
"""API调用结果数据类"""
|
||||
success: bool
|
||||
data: Optional[Any] = None
|
||||
provider_used: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
class OpenBBEngine:
|
||||
"""OpenBB 集成引擎"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
初始化 OpenBB 引擎
|
||||
"""
|
||||
# 延迟导入 OpenBB,避免未安装时报错
|
||||
self._obb = None
|
||||
|
||||
# 八仙专属数据源分配
|
||||
self.immortal_sources: Dict[str, ImmortalConfig] = {
|
||||
'吕洞宾': ImmortalConfig( # 乾-技术分析专家
|
||||
primary='yfinance',
|
||||
specialty='technical_analysis'
|
||||
),
|
||||
'何仙姑': ImmortalConfig( # 坤-风险控制专家
|
||||
primary='yfinance',
|
||||
specialty='risk_metrics'
|
||||
),
|
||||
'张果老': ImmortalConfig( # 兑-历史数据分析师
|
||||
primary='yfinance',
|
||||
specialty='historical_data'
|
||||
),
|
||||
'韩湘子': ImmortalConfig( # 艮-新兴资产专家
|
||||
primary='yfinance',
|
||||
specialty='sector_analysis'
|
||||
),
|
||||
'汉钟离': ImmortalConfig( # 离-热点追踪
|
||||
primary='yfinance',
|
||||
specialty='market_movers'
|
||||
),
|
||||
'蓝采和': ImmortalConfig( # 坎-潜力股发现
|
||||
primary='yfinance',
|
||||
specialty='screener'
|
||||
),
|
||||
'曹国舅': ImmortalConfig( # 震-机构分析
|
||||
primary='yfinance',
|
||||
specialty='institutional_holdings'
|
||||
),
|
||||
'铁拐李': ImmortalConfig( # 巽-逆向投资
|
||||
primary='yfinance',
|
||||
specialty='short_interest'
|
||||
)
|
||||
}
|
||||
|
||||
print("✅ OpenBB 引擎初始化完成")
|
||||
|
||||
def _ensure_openbb(self):
|
||||
"""Lazy import OpenBB v4 obb router."""
|
||||
if self._obb is not None:
|
||||
return True
|
||||
try:
|
||||
from openbb import obb # type: ignore
|
||||
self._obb = obb
|
||||
return True
|
||||
except Exception:
|
||||
self._obb = None
|
||||
return False
|
||||
|
||||
def get_immortal_data(self, immortal_name: str, data_type: str, symbol: str = 'AAPL') -> APIResult:
|
||||
"""
|
||||
为特定八仙获取专属数据
|
||||
|
||||
Args:
|
||||
immortal_name: 八仙名称
|
||||
data_type: 数据类型
|
||||
symbol: 股票代码
|
||||
|
||||
Returns:
|
||||
API调用结果
|
||||
"""
|
||||
if immortal_name not in self.immortal_sources:
|
||||
return APIResult(success=False, error=f'Unknown immortal: {immortal_name}')
|
||||
|
||||
immortal_config = self.immortal_sources[immortal_name]
|
||||
|
||||
print(f"🧙♂️ {immortal_name} 请求 {data_type} 数据 (股票: {symbol})")
|
||||
|
||||
# 根据数据类型调用不同的 OpenBB 函数
|
||||
try:
|
||||
if not self._ensure_openbb():
|
||||
return APIResult(success=False, error='OpenBB 未安装,请先安装 openbb>=4 并在 requirements.txt 启用')
|
||||
obb = self._obb
|
||||
if data_type == 'price':
|
||||
result = obb.equity.price.quote(symbol=symbol, provider=immortal_config.primary)
|
||||
return APIResult(
|
||||
success=True,
|
||||
data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()),
|
||||
provider_used=immortal_config.primary
|
||||
)
|
||||
elif data_type == 'historical':
|
||||
result = obb.equity.price.historical(symbol=symbol, provider=immortal_config.primary)
|
||||
return APIResult(
|
||||
success=True,
|
||||
data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()),
|
||||
provider_used=immortal_config.primary
|
||||
)
|
||||
elif data_type == 'profile':
|
||||
result = obb.equity.profile(symbol=symbol, provider=immortal_config.primary)
|
||||
return APIResult(
|
||||
success=True,
|
||||
data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()),
|
||||
provider_used=immortal_config.primary
|
||||
)
|
||||
elif data_type == 'news':
|
||||
result = obb.news.company(symbol=symbol)
|
||||
return APIResult(
|
||||
success=True,
|
||||
data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()),
|
||||
provider_used='news_api'
|
||||
)
|
||||
elif data_type == 'earnings':
|
||||
result = obb.equity.earnings.earnings_historical(symbol=symbol, provider=immortal_config.primary)
|
||||
return APIResult(
|
||||
success=True,
|
||||
data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()),
|
||||
provider_used=immortal_config.primary
|
||||
)
|
||||
elif data_type == 'dividends':
|
||||
result = obb.equity.fundamental.dividend(symbol=symbol, provider=immortal_config.primary)
|
||||
return APIResult(
|
||||
success=True,
|
||||
data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()),
|
||||
provider_used=immortal_config.primary
|
||||
)
|
||||
elif data_type == 'screener':
|
||||
# 使用简单的筛选器作为替代
|
||||
result = obb.equity.screener.etf(
|
||||
provider=immortal_config.primary
|
||||
)
|
||||
return APIResult(
|
||||
success=True,
|
||||
data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()),
|
||||
provider_used=immortal_config.primary
|
||||
)
|
||||
else:
|
||||
return APIResult(success=False, error=f'Unsupported data type: {data_type}')
|
||||
|
||||
except Exception as e:
|
||||
return APIResult(success=False, error=f'OpenBB 调用失败: {str(e)}')
|
||||
|
||||
def simulate_jixia_debate(self, topic_symbol: str = 'TSLA') -> Dict[str, APIResult]:
|
||||
"""
|
||||
模拟稷下学宫八仙论道
|
||||
|
||||
Args:
|
||||
topic_symbol: 辩论主题股票代码
|
||||
|
||||
Returns:
|
||||
八仙辩论结果
|
||||
"""
|
||||
print(f"🏛️ 稷下学宫八仙论道 - 主题: {topic_symbol} (OpenBB 版本)")
|
||||
print("=" * 60)
|
||||
|
||||
debate_results: Dict[str, APIResult] = {}
|
||||
|
||||
# 数据类型映射
|
||||
data_type_mapping = {
|
||||
'technical_analysis': 'historical', # 技术分析使用历史价格数据
|
||||
'risk_metrics': 'price', # 风险控制使用当前价格数据
|
||||
'historical_data': 'historical', # 历史数据分析使用历史价格数据
|
||||
'sector_analysis': 'profile', # 新兴资产分析使用公司概况
|
||||
'market_movers': 'news', # 热点追踪使用新闻
|
||||
'screener': 'screener', # 潜力股发现使用筛选器
|
||||
'institutional_holdings': 'profile', # 机构分析使用公司概况
|
||||
'short_interest': 'profile' # 逆向投资使用公司概况
|
||||
}
|
||||
|
||||
# 八仙依次发言
|
||||
for immortal_name, config in self.immortal_sources.items():
|
||||
print(f"\n🎭 {immortal_name} ({config.specialty}) 发言:")
|
||||
|
||||
data_type = data_type_mapping.get(config.specialty, 'price')
|
||||
result = self.get_immortal_data(immortal_name, data_type, topic_symbol)
|
||||
|
||||
if result.success:
|
||||
debate_results[immortal_name] = result
|
||||
print(f" 💬 观点: 基于{result.provider_used}数据的{config.specialty}分析")
|
||||
# 显示部分数据示例
|
||||
if result.data:
|
||||
if isinstance(result.data, list) and len(result.data) > 0:
|
||||
sample = result.data[0]
|
||||
print(f" 📊 数据示例: {sample}")
|
||||
elif hasattr(result.data, '__dict__'):
|
||||
# 如果是对象,显示前几个属性
|
||||
attrs = vars(result.data)
|
||||
sample = {k: v for k, v in list(attrs.items())[:3]}
|
||||
print(f" 📊 数据示例: {sample}")
|
||||
else:
|
||||
print(f" 📊 数据示例: {result.data}")
|
||||
else:
|
||||
print(f" 😔 暂时无法获取数据: {result.error}")
|
||||
|
||||
return debate_results
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试 OpenBB 引擎
|
||||
print("🧪 OpenBB 引擎测试")
|
||||
engine = OpenBBEngine()
|
||||
engine.simulate_jixia_debate('AAPL')
|
||||
161
jixia_academy/core/debate_system/engines/openbb_stock_data.py
Normal file
161
jixia_academy/core/debate_system/engines/openbb_stock_data.py
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenBB 股票数据获取模块
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
def get_stock_data(symbol: str, days: int = 90) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
获取指定股票在指定天数内的历史数据
|
||||
|
||||
Args:
|
||||
symbol (str): 股票代码 (如 'AAPL')
|
||||
days (int): 时间窗口(天),默认90天
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 股票历史数据列表,如果失败则返回None
|
||||
"""
|
||||
try:
|
||||
# 计算开始日期
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
print(f"🔍 正在获取 {symbol} 近 {days} 天的数据...")
|
||||
print(f" 时间范围: {start_date.strftime('%Y-%m-%d')} 到 {end_date.strftime('%Y-%m-%d')}")
|
||||
|
||||
# 使用OpenBB获取数据(延迟导入)
|
||||
try:
|
||||
from openbb import obb # type: ignore
|
||||
except Exception as e:
|
||||
print(f"⚠️ OpenBB 未安装或导入失败: {e}")
|
||||
return None
|
||||
|
||||
result = obb.equity.price.historical(
|
||||
symbol=symbol,
|
||||
provider='yfinance',
|
||||
start_date=start_date.strftime('%Y-%m-%d'),
|
||||
end_date=end_date.strftime('%Y-%m-%d')
|
||||
)
|
||||
|
||||
results = getattr(result, 'results', None)
|
||||
if results:
|
||||
print(f"✅ 成功获取 {len(results)} 条记录")
|
||||
return results
|
||||
else:
|
||||
print("❌ 未获取到数据")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 获取数据时出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_etf_data(symbol: str, days: int = 90) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
获取指定ETF在指定天数内的历史数据
|
||||
|
||||
Args:
|
||||
symbol (str): ETF代码 (如 'SPY')
|
||||
days (int): 时间窗口(天),默认90天
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: ETF历史数据列表,如果失败则返回None
|
||||
"""
|
||||
try:
|
||||
# 计算开始日期
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
print(f"🔍 正在获取 {symbol} 近 {days} 天的数据...")
|
||||
print(f" 时间范围: {start_date.strftime('%Y-%m-%d')} 到 {end_date.strftime('%Y-%m-%d')}")
|
||||
|
||||
# 使用OpenBB获取数据(延迟导入)
|
||||
try:
|
||||
from openbb import obb # type: ignore
|
||||
except Exception as e:
|
||||
print(f"⚠️ OpenBB 未安装或导入失败: {e}")
|
||||
return None
|
||||
|
||||
result = obb.etf.price.historical(
|
||||
symbol=symbol,
|
||||
provider='yfinance',
|
||||
start_date=start_date.strftime('%Y-%m-%d'),
|
||||
end_date=end_date.strftime('%Y-%m-%d')
|
||||
)
|
||||
|
||||
results = getattr(result, 'results', None)
|
||||
if results:
|
||||
print(f"✅ 成功获取 {len(results)} 条记录")
|
||||
return results
|
||||
else:
|
||||
print("❌ 未获取到数据")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 获取数据时出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def format_stock_data(data: List[Dict[str, Any]]) -> None:
|
||||
"""
|
||||
格式化并打印股票数据
|
||||
|
||||
Args:
|
||||
data (List[Dict[str, Any]]): 股票数据列表
|
||||
"""
|
||||
if not data:
|
||||
print("😔 没有数据可显示")
|
||||
return
|
||||
|
||||
print(f"\n📊 股票数据预览 (显示最近5条记录):")
|
||||
print("-" * 80)
|
||||
print(f"{'日期':<12} {'开盘':<10} {'最高':<10} {'最低':<10} {'收盘':<10} {'成交量':<15}")
|
||||
print("-" * 80)
|
||||
|
||||
# 只显示最近5条记录
|
||||
for item in data[-5:]:
|
||||
print(f"{str(item.date):<12} {item.open:<10.2f} {item.high:<10.2f} {item.low:<10.2f} {item.close:<10.2f} {item.volume:<15,}")
|
||||
|
||||
def format_etf_data(data: List[Dict[str, Any]]) -> None:
|
||||
"""
|
||||
格式化并打印ETF数据
|
||||
|
||||
Args:
|
||||
data (List[Dict[str, Any]]): ETF数据列表
|
||||
"""
|
||||
if not data:
|
||||
print("😔 没有数据可显示")
|
||||
return
|
||||
|
||||
print(f"\n📊 ETF数据预览 (显示最近5条记录):")
|
||||
print("-" * 80)
|
||||
print(f"{'日期':<12} {'开盘':<10} {'最高':<10} {'最低':<10} {'收盘':<10} {'成交量':<15}")
|
||||
print("-" * 80)
|
||||
|
||||
# 只显示最近5条记录
|
||||
for item in data[-5:]:
|
||||
print(f"{str(item.date):<12} {item.open:<10.2f} {item.high:<10.2f} {item.low:<10.2f} {item.close:<10.2f} {item.volume:<15,}")
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 示例:获取AAPL股票和SPY ETF的数据
|
||||
symbols = [("AAPL", "stock"), ("SPY", "etf")]
|
||||
time_windows = [90, 720]
|
||||
|
||||
for symbol, asset_type in symbols:
|
||||
for days in time_windows:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"获取 {symbol} {days} 天数据")
|
||||
print(f"{'='*60}")
|
||||
|
||||
if asset_type == "stock":
|
||||
data = get_stock_data(symbol, days)
|
||||
if data:
|
||||
format_stock_data(data)
|
||||
else:
|
||||
data = get_etf_data(symbol, days)
|
||||
if data:
|
||||
format_etf_data(data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
329
jixia_academy/core/debate_system/engines/perpetual_engine.py
Normal file
329
jixia_academy/core/debate_system/engines/perpetual_engine.py
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
稷下学宫永动机引擎
|
||||
为八仙论道提供无限数据支撑
|
||||
|
||||
重构版本:
|
||||
- 移除硬编码密钥
|
||||
- 添加类型注解
|
||||
- 改进错误处理
|
||||
- 统一配置管理
|
||||
"""
|
||||
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class ImmortalConfig:
|
||||
"""八仙配置数据类"""
|
||||
primary: str
|
||||
backup: List[str]
|
||||
specialty: str
|
||||
|
||||
@dataclass
|
||||
class APIResult:
|
||||
"""API调用结果数据类"""
|
||||
success: bool
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
api_used: Optional[str] = None
|
||||
usage_count: Optional[int] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
class JixiaPerpetualEngine:
|
||||
"""稷下学宫永动机引擎"""
|
||||
|
||||
def __init__(self, rapidapi_key: str):
|
||||
"""
|
||||
初始化永动机引擎
|
||||
|
||||
Args:
|
||||
rapidapi_key: RapidAPI密钥,从环境变量或Doppler获取
|
||||
"""
|
||||
if not rapidapi_key:
|
||||
raise ValueError("RapidAPI密钥不能为空")
|
||||
|
||||
self.rapidapi_key = rapidapi_key
|
||||
|
||||
# 八仙专属API分配 - 基于4个可用API优化
|
||||
self.immortal_apis: Dict[str, ImmortalConfig] = {
|
||||
'吕洞宾': ImmortalConfig( # 乾-技术分析专家
|
||||
primary='alpha_vantage',
|
||||
backup=['yahoo_finance_1'],
|
||||
specialty='comprehensive_analysis'
|
||||
),
|
||||
'何仙姑': ImmortalConfig( # 坤-风险控制专家
|
||||
primary='yahoo_finance_1',
|
||||
backup=['webull'],
|
||||
specialty='risk_management'
|
||||
),
|
||||
'张果老': ImmortalConfig( # 兑-历史数据分析师
|
||||
primary='seeking_alpha',
|
||||
backup=['alpha_vantage'],
|
||||
specialty='fundamental_analysis'
|
||||
),
|
||||
'韩湘子': ImmortalConfig( # 艮-新兴资产专家
|
||||
primary='webull',
|
||||
backup=['yahoo_finance_1'],
|
||||
specialty='emerging_trends'
|
||||
),
|
||||
'汉钟离': ImmortalConfig( # 离-热点追踪
|
||||
primary='yahoo_finance_1',
|
||||
backup=['webull'],
|
||||
specialty='hot_trends'
|
||||
),
|
||||
'蓝采和': ImmortalConfig( # 坎-潜力股发现
|
||||
primary='webull',
|
||||
backup=['alpha_vantage'],
|
||||
specialty='undervalued_stocks'
|
||||
),
|
||||
'曹国舅': ImmortalConfig( # 震-机构分析
|
||||
primary='seeking_alpha',
|
||||
backup=['alpha_vantage'],
|
||||
specialty='institutional_analysis'
|
||||
),
|
||||
'铁拐李': ImmortalConfig( # 巽-逆向投资
|
||||
primary='alpha_vantage',
|
||||
backup=['seeking_alpha'],
|
||||
specialty='contrarian_analysis'
|
||||
)
|
||||
}
|
||||
|
||||
# API池配置 - 只保留4个可用的API
|
||||
self.api_configs: Dict[str, str] = {
|
||||
'alpha_vantage': 'alpha-vantage.p.rapidapi.com', # 1.26s ⚡
|
||||
'webull': 'webull.p.rapidapi.com', # 1.56s ⚡
|
||||
'yahoo_finance_1': 'yahoo-finance15.p.rapidapi.com', # 2.07s
|
||||
'seeking_alpha': 'seeking-alpha.p.rapidapi.com' # 3.32s
|
||||
}
|
||||
|
||||
# 使用统计
|
||||
self.usage_tracker: Dict[str, int] = {api: 0 for api in self.api_configs.keys()}
|
||||
|
||||
def get_immortal_data(self, immortal_name: str, data_type: str, symbol: str = 'AAPL') -> APIResult:
|
||||
"""
|
||||
为特定八仙获取专属数据
|
||||
|
||||
Args:
|
||||
immortal_name: 八仙名称
|
||||
data_type: 数据类型
|
||||
symbol: 股票代码
|
||||
|
||||
Returns:
|
||||
API调用结果
|
||||
"""
|
||||
if immortal_name not in self.immortal_apis:
|
||||
return APIResult(success=False, error=f'Unknown immortal: {immortal_name}')
|
||||
|
||||
immortal_config = self.immortal_apis[immortal_name]
|
||||
|
||||
print(f"🧙♂️ {immortal_name} 请求 {data_type} 数据 (股票: {symbol})")
|
||||
|
||||
# 尝试主要API
|
||||
result = self._call_api(immortal_config.primary, data_type, symbol)
|
||||
if result.success:
|
||||
print(f" ✅ 使用主要API: {immortal_config.primary}")
|
||||
return result
|
||||
|
||||
# 故障转移到备用API
|
||||
for backup_api in immortal_config.backup:
|
||||
print(f" 🔄 故障转移到: {backup_api}")
|
||||
result = self._call_api(backup_api, data_type, symbol)
|
||||
if result.success:
|
||||
print(f" ✅ 备用API成功: {backup_api}")
|
||||
return result
|
||||
|
||||
print(f" ❌ 所有API都失败了")
|
||||
return APIResult(success=False, error='All APIs failed')
|
||||
|
||||
def _call_api(self, api_name: str, data_type: str, symbol: str) -> APIResult:
|
||||
"""
|
||||
调用指定API
|
||||
|
||||
Args:
|
||||
api_name: API名称
|
||||
data_type: 数据类型
|
||||
symbol: 股票代码
|
||||
|
||||
Returns:
|
||||
API调用结果
|
||||
"""
|
||||
if api_name not in self.api_configs:
|
||||
return APIResult(success=False, error=f'API {api_name} not configured')
|
||||
|
||||
host = self.api_configs[api_name]
|
||||
headers = {
|
||||
'X-RapidAPI-Key': self.rapidapi_key,
|
||||
'X-RapidAPI-Host': host,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
endpoint = self._get_endpoint(api_name, data_type, symbol)
|
||||
if not endpoint:
|
||||
return APIResult(success=False, error=f'No endpoint for {data_type} on {api_name}')
|
||||
|
||||
url = f"https://{host}{endpoint}"
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=8)
|
||||
self.usage_tracker[api_name] += 1
|
||||
|
||||
if response.status_code == 200:
|
||||
return APIResult(
|
||||
success=True,
|
||||
data=response.json(),
|
||||
api_used=api_name,
|
||||
usage_count=self.usage_tracker[api_name]
|
||||
)
|
||||
else:
|
||||
return APIResult(
|
||||
success=False,
|
||||
error=f'HTTP {response.status_code}: {response.text[:100]}'
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
return APIResult(success=False, error='Request timeout')
|
||||
except requests.exceptions.RequestException as e:
|
||||
return APIResult(success=False, error=f'Request error: {str(e)}')
|
||||
except Exception as e:
|
||||
return APIResult(success=False, error=f'Unexpected error: {str(e)}')
|
||||
|
||||
def _get_endpoint(self, api_name: str, data_type: str, symbol: str) -> Optional[str]:
|
||||
"""
|
||||
根据API和数据类型返回合适的端点
|
||||
|
||||
Args:
|
||||
api_name: API名称
|
||||
data_type: 数据类型
|
||||
symbol: 股票代码
|
||||
|
||||
Returns:
|
||||
API端点路径
|
||||
"""
|
||||
endpoint_mapping = {
|
||||
'alpha_vantage': {
|
||||
'quote': f'/query?function=GLOBAL_QUOTE&symbol={symbol}',
|
||||
'overview': f'/query?function=OVERVIEW&symbol={symbol}',
|
||||
'earnings': f'/query?function=EARNINGS&symbol={symbol}',
|
||||
'profile': f'/query?function=OVERVIEW&symbol={symbol}',
|
||||
'analysis': f'/query?function=OVERVIEW&symbol={symbol}'
|
||||
},
|
||||
'yahoo_finance_1': {
|
||||
'quote': f'/api/yahoo/qu/quote/{symbol}',
|
||||
'gainers': '/api/yahoo/co/collections/day_gainers',
|
||||
'losers': '/api/yahoo/co/collections/day_losers',
|
||||
'search': f'/api/yahoo/qu/quote/{symbol}',
|
||||
'analysis': f'/api/yahoo/qu/quote/{symbol}',
|
||||
'profile': f'/api/yahoo/qu/quote/{symbol}'
|
||||
},
|
||||
'seeking_alpha': {
|
||||
'profile': f'/symbols/get-profile?symbols={symbol}',
|
||||
'news': '/news/list?category=market-news',
|
||||
'analysis': f'/symbols/get-profile?symbols={symbol}',
|
||||
'quote': f'/symbols/get-profile?symbols={symbol}'
|
||||
},
|
||||
'webull': {
|
||||
'search': f'/stock/search?keyword={symbol}',
|
||||
'quote': f'/stock/search?keyword={symbol}',
|
||||
'analysis': f'/stock/search?keyword={symbol}',
|
||||
'gainers': '/market/get-active-gainers',
|
||||
'profile': f'/stock/search?keyword={symbol}'
|
||||
}
|
||||
}
|
||||
|
||||
api_endpoints = endpoint_mapping.get(api_name, {})
|
||||
return api_endpoints.get(data_type, api_endpoints.get('quote'))
|
||||
|
||||
def simulate_jixia_debate(self, topic_symbol: str = 'TSLA') -> Dict[str, APIResult]:
|
||||
"""
|
||||
模拟稷下学宫八仙论道
|
||||
|
||||
Args:
|
||||
topic_symbol: 辩论主题股票代码
|
||||
|
||||
Returns:
|
||||
八仙辩论结果
|
||||
"""
|
||||
print(f"🏛️ 稷下学宫八仙论道 - 主题: {topic_symbol}")
|
||||
print("=" * 60)
|
||||
|
||||
debate_results: Dict[str, APIResult] = {}
|
||||
|
||||
# 数据类型映射
|
||||
data_type_mapping = {
|
||||
'comprehensive_analysis': 'overview',
|
||||
'etf_tracking': 'quote',
|
||||
'fundamental_analysis': 'profile',
|
||||
'emerging_trends': 'news',
|
||||
'hot_trends': 'gainers',
|
||||
'undervalued_stocks': 'search',
|
||||
'institutional_analysis': 'profile',
|
||||
'contrarian_analysis': 'analysis'
|
||||
}
|
||||
|
||||
# 八仙依次发言
|
||||
for immortal_name, config in self.immortal_apis.items():
|
||||
print(f"\n🎭 {immortal_name} ({config.specialty}) 发言:")
|
||||
|
||||
data_type = data_type_mapping.get(config.specialty, 'quote')
|
||||
result = self.get_immortal_data(immortal_name, data_type, topic_symbol)
|
||||
|
||||
if result.success:
|
||||
debate_results[immortal_name] = result
|
||||
print(f" 💬 观点: 基于{result.api_used}数据的{config.specialty}分析")
|
||||
else:
|
||||
print(f" 😔 暂时无法获取数据: {result.error}")
|
||||
|
||||
time.sleep(0.5) # 避免过快请求
|
||||
|
||||
return debate_results
|
||||
|
||||
def get_usage_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取使用统计信息
|
||||
|
||||
Returns:
|
||||
统计信息字典
|
||||
"""
|
||||
total_calls = sum(self.usage_tracker.values())
|
||||
active_apis = len([api for api, count in self.usage_tracker.items() if count > 0])
|
||||
unused_apis = [api for api, count in self.usage_tracker.items() if count == 0]
|
||||
|
||||
return {
|
||||
'total_calls': total_calls,
|
||||
'active_apis': active_apis,
|
||||
'total_apis': len(self.api_configs),
|
||||
'average_calls_per_api': total_calls / len(self.api_configs) if self.api_configs else 0,
|
||||
'usage_by_api': {api: count for api, count in self.usage_tracker.items() if count > 0},
|
||||
'unused_apis': unused_apis,
|
||||
'unused_count': len(unused_apis)
|
||||
}
|
||||
|
||||
def print_perpetual_stats(self) -> None:
|
||||
"""打印永动机统计信息"""
|
||||
stats = self.get_usage_stats()
|
||||
|
||||
print(f"\n📊 永动机运行统计:")
|
||||
print("=" * 60)
|
||||
print(f"总API调用次数: {stats['total_calls']}")
|
||||
print(f"活跃API数量: {stats['active_apis']}/{stats['total_apis']}")
|
||||
print(f"平均每API调用: {stats['average_calls_per_api']:.1f}次")
|
||||
|
||||
if stats['usage_by_api']:
|
||||
print(f"\n各API使用情况:")
|
||||
for api, count in stats['usage_by_api'].items():
|
||||
print(f" {api}: {count}次")
|
||||
|
||||
print(f"\n🎯 未使用的API储备: {stats['unused_count']}个")
|
||||
if stats['unused_apis']:
|
||||
unused_display = ', '.join(stats['unused_apis'][:5])
|
||||
if len(stats['unused_apis']) > 5:
|
||||
unused_display += '...'
|
||||
print(f"储备API: {unused_display}")
|
||||
|
||||
print(f"\n💡 永动机效果:")
|
||||
print(f" • {stats['total_apis']}个API订阅,智能调度")
|
||||
print(f" • 智能故障转移,永不断线")
|
||||
print(f" • 八仙专属API,个性化数据")
|
||||
print(f" • 成本优化,效果最大化!")
|
||||
48
jixia_academy/core/debate_system/engines/rapidapi_adapter.py
Normal file
48
jixia_academy/core/debate_system/engines/rapidapi_adapter.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from typing import List, Optional
|
||||
from src.jixia.engines.data_abstraction import DataProvider
|
||||
from src.jixia.models.financial_data_models import StockQuote, HistoricalPrice, CompanyProfile, FinancialNews
|
||||
from src.jixia.engines.perpetual_engine import JixiaPerpetualEngine
|
||||
from config.settings import get_rapidapi_key
|
||||
|
||||
class RapidAPIDataProvider(DataProvider):
|
||||
"""RapidAPI永动机引擎适配器"""
|
||||
|
||||
def __init__(self):
|
||||
self.engine = JixiaPerpetualEngine(get_rapidapi_key())
|
||||
self._name = "RapidAPI"
|
||||
self._priority = 2 # 中等优先级
|
||||
|
||||
def get_quote(self, symbol: str) -> Optional[StockQuote]:
|
||||
result = self.engine.get_immortal_data("吕洞宾", "quote", symbol)
|
||||
if result.success and result.data:
|
||||
# 解析RapidAPI返回的数据并转换为StockQuote
|
||||
# 这里需要根据实际API返回的数据结构进行调整
|
||||
return StockQuote(
|
||||
symbol=symbol,
|
||||
price=result.data.get("price", 0),
|
||||
change=result.data.get("change", 0),
|
||||
change_percent=result.data.get("change_percent", 0),
|
||||
volume=result.data.get("volume", 0),
|
||||
timestamp=result.data.get("timestamp")
|
||||
)
|
||||
return None
|
||||
|
||||
def get_historical_prices(self, symbol: str, days: int = 30) -> List[HistoricalPrice]:
|
||||
# 实现历史价格数据获取逻辑
|
||||
pass
|
||||
|
||||
def get_company_profile(self, symbol: str) -> Optional[CompanyProfile]:
|
||||
# 实现公司概况获取逻辑
|
||||
pass
|
||||
|
||||
def get_news(self, symbol: str, limit: int = 10) -> List[FinancialNews]:
|
||||
# 实现新闻获取逻辑
|
||||
pass
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return self._priority
|
||||
Reference in New Issue
Block a user