ADD: 支持ETF、 LOF基金!

This commit is contained in:
兰志宏
2025-03-05 16:32:30 +08:00
parent f5ebe29782
commit 25601b9bd6
4 changed files with 273 additions and 93 deletions

51
fund_service.py Normal file
View File

@@ -0,0 +1,51 @@
import akshare as ak
import pandas as pd
class FundService:
def search_funds(self, keyword, market_type='ETF'):
"""
搜索基金代码
:param keyword: 搜索关键词
:return: 匹配的基金列表
"""
try:
# 获取ETF和LOF数据
if market_type == 'ETF':
df = ak.fund_etf_spot_em()
else:
df = ak.fund_lof_spot_em()
# 转换列名
df = df.rename(columns={
"代码": "symbol",
"名称": "name",
"最新价": "price",
"涨跌额": "price_change",
"涨跌幅": "price_change_percent",
"成交量": "volume",
"流通市值": "market_value",
"总市值": "total_value",
"基金折价率": "discount_rate",
})
# 模糊匹配搜索(同时匹配代码和名称)
mask = (df['name'].str.contains(keyword, case=False, na=False) |
df['symbol'].str.contains(keyword, case=False, na=False))
results = df[mask]
# 格式化返回结果并处理 NaN 值
formatted_results = []
for _, row in results.iterrows():
formatted_results.append({
'name': row['name'] if pd.notna(row['name']) else '',
'symbol': str(row['symbol']) if pd.notna(row['symbol']) else '',
'price': float(row['price']) if pd.notna(row['price']) else 0.0,
'volume': float(row['volume']) if pd.notna(row['volume']) else 0.0,
'market_value': float(row['market_value']) if pd.notna(row['market_value']) else 0.0,
'total_value': float(row['total_value']) if pd.notna(row['total_value']) else 0.0,
})
return formatted_results
except Exception as e:
raise Exception(f"搜索基金代码失败: {str(e)}")

View File

@@ -37,8 +37,8 @@ class StockAnalyzer:
} }
def get_stock_data(self, stock_code, market_type='A', start_date=None, end_date=None, ): def get_stock_data(self, stock_code, market_type='A', start_date=None, end_date=None):
"""获取股票数据""" """获取股票或基金数据"""
import akshare as ak import akshare as ak
if start_date is None: if start_date is None:
@@ -55,7 +55,6 @@ class StockAnalyzer:
end_date=end_date, end_date=end_date,
adjust="qfq" adjust="qfq"
) )
# A股数据列名映射
elif market_type == 'HK': elif market_type == 'HK':
df = ak.stock_hk_daily( df = ak.stock_hk_daily(
symbol=stock_code, symbol=stock_code,
@@ -68,10 +67,22 @@ class StockAnalyzer:
end_date=end_date, end_date=end_date,
adjust="qfq" adjust="qfq"
) )
# elif market_type == 'CRYPTO': elif market_type == 'ETF':
# df = ak.crypto_js_spot( df = ak.fund_etf_hist_em(
# symbol=stock_code symbol=stock_code,
# ) period="daily",
start_date=start_date,
end_date=end_date,
adjust="qfq"
)
elif market_type == 'LOF':
df = ak.fund_lof_hist_em(
symbol=stock_code,
period="daily",
start_date=start_date,
end_date=end_date,
adjust="qfq"
)
else: else:
raise ValueError(f"不支持的市场类型: {market_type}") raise ValueError(f"不支持的市场类型: {market_type}")
@@ -214,10 +225,10 @@ class StockAnalyzer:
print(f"计算评分时出错: {str(e)}") print(f"计算评分时出错: {str(e)}")
raise raise
def get_ai_analysis(self, df, stock_code, stream=False): def get_ai_analysis(self, df, stock_code, market_type='A', stream=False):
"""使用 OpenAI 进行 AI 分析""" """使用 OpenAI 进行 AI 分析"""
try: try:
logger.info(f"开始AI分析股票 {stock_code}, 流式模式: {stream}") logger.info(f"开始AI分析 {stock_code}, 流式模式: {stream}")
recent_data = df.tail(14).to_dict('records') recent_data = df.tail(14).to_dict('records')
technical_summary = { technical_summary = {
@@ -227,8 +238,70 @@ class StockAnalyzer:
'rsi_level': df.iloc[-1]['RSI'] 'rsi_level': df.iloc[-1]['RSI']
} }
# 根据市场类型调整分析提示
if market_type in ['ETF', 'LOF']:
prompt = f""" prompt = f"""
分析股票 {stock_code} 分析基金 {stock_code}
技术指标概要:
{technical_summary}
近14日交易数据
{recent_data}
请提供:
1. 净值走势分析(包含支撑位和压力位)
2. 成交量分析及其对净值的影响
3. 风险评估(包含波动率和折溢价分析)
4. 短期和中期净值预测
5. 关键价格位分析
6. 申购赎回建议(包含止损位)
请基于技术指标和市场表现进行分析,给出具体数据支持。
"""
elif market_type == 'US':
prompt = f"""
分析美股 {stock_code}
技术指标概要:
{technical_summary}
近14日交易数据
{recent_data}
请提供:
1. 趋势分析(包含支撑位和压力位,美元计价)
2. 成交量分析及其含义
3. 风险评估(包含波动率和美股市场特有风险)
4. 短期和中期目标价位(美元)
5. 关键技术位分析
6. 具体交易建议(包含止损位)
请基于技术指标和美股市场特点进行分析,给出具体数据支持。
"""
elif market_type == 'HK':
prompt = f"""
分析港股 {stock_code}
技术指标概要:
{technical_summary}
近14日交易数据
{recent_data}
请提供:
1. 趋势分析(包含支撑位和压力位,港币计价)
2. 成交量分析及其含义
3. 风险评估(包含波动率和港股市场特有风险)
4. 短期和中期目标价位(港币)
5. 关键技术位分析
6. 具体交易建议(包含止损位)
请基于技术指标和港股市场特点进行分析,给出具体数据支持。
"""
else: # A股
prompt = f"""
分析A股 {stock_code}
技术指标概要: 技术指标概要:
{technical_summary} {technical_summary}
@@ -244,7 +317,7 @@ class StockAnalyzer:
5. 关键技术位分析 5. 关键技术位分析
6. 具体交易建议(包含止损位) 6. 具体交易建议(包含止损位)
请基于技术指标和市场动态进行分析,给出具体数据支持。 请基于技术指标和A股市场特点进行分析,给出具体数据支持。
""" """
logger.debug(f"生成的AI分析提示词: {self._truncate_json_for_logging(prompt, 100)}...") logger.debug(f"生成的AI分析提示词: {self._truncate_json_for_logging(prompt, 100)}...")
@@ -381,7 +454,7 @@ class StockAnalyzer:
def _process_ai_stream(self, response, stock_code) -> Generator[str, None, None]: def _process_ai_stream(self, response, stock_code) -> Generator[str, None, None]:
"""处理AI流式响应""" """处理AI流式响应"""
logger.info(f"开始处理股票 {stock_code} 的AI流式响应") logger.info(f"开始处理 {stock_code} 的AI流式响应")
buffer = "" buffer = ""
chunk_count = 0 chunk_count = 0
@@ -457,49 +530,45 @@ class StockAnalyzer:
return '强烈建议卖出' return '强烈建议卖出'
def analyze_stock(self, stock_code, market_type='A', stream=False): def analyze_stock(self, stock_code, market_type='A', stream=False):
"""分析单个股票""" """分析股票或基金"""
try: try:
logger.info(f"开始分析股票: {stock_code}, 市场: {market_type}, 流式模式: {stream}") logger.info(f"开始分析: {stock_code}, 市场: {market_type}, 流式模式: {stream}")
# 获取股票数据 # 获取数据
logger.debug(f"获取股票 {stock_code} 数据") logger.debug(f"获取 {stock_code} 数据")
df = self.get_stock_data(stock_code, market_type) df = self.get_stock_data(stock_code, market_type)
# 计算技术指标 # 计算技术指标
logger.debug(f"计算股票 {stock_code} 技术指标") logger.debug(f"计算 {stock_code} 技术指标")
df = self.calculate_indicators(df) df = self.calculate_indicators(df)
# 评分系统 # 评分系统
logger.debug(f"计算股票 {stock_code} 评分") logger.debug(f"计算 {stock_code} 评分")
score = self.calculate_score(df) score = self.calculate_score(df)
logger.info(f"股票 {stock_code} 评分结果: {score}") logger.info(f"{stock_code} 评分结果: {score}")
# 获取最新数据 # 获取最新数据
latest = df.iloc[-1] latest = df.iloc[-1]
prev = df.iloc[-2] prev = df.iloc[-2]
# 处理 RSI 的 NaN 值 # 生成报告
rsi_value = latest['RSI']
if pd.isna(rsi_value):
rsi_value = None
# 生成报告(保持原有格式)
report = { report = {
'stock_code': stock_code, 'stock_code': stock_code,
'market_type': market_type, # 添加市场类型
'analysis_date': datetime.now().strftime('%Y-%m-%d'), 'analysis_date': datetime.now().strftime('%Y-%m-%d'),
'score': score, 'score': score,
'price': latest['close'], 'price': latest['close'],
'price_change': (latest['close'] - prev['close']) / prev['close'] * 100, 'price_change': (latest['close'] - prev['close']) / prev['close'] * 100,
'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN', 'ma_trend': 'UP' if latest['MA5'] > latest['MA20'] else 'DOWN',
'rsi': rsi_value, # 使用处理后的 RSI 值 'rsi': latest['RSI'] if not pd.isna(latest['RSI']) else None,
'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL', 'macd_signal': 'BUY' if latest['MACD'] > latest['Signal'] else 'SELL',
'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL', 'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL',
'recommendation': self.get_recommendation(score) 'recommendation': self.get_recommendation(score)
} }
logger.debug(f"生成股票 {stock_code} 基础报告: {self._truncate_json_for_logging(report, 100)}...") logger.debug(f"生成 {stock_code} 基础报告: {self._truncate_json_for_logging(report, 100)}...")
if stream: if stream:
logger.info(f"以流式模式返回股票 {stock_code} 分析结果") logger.info(f"以流式模式返回 {stock_code} 分析结果")
# 先返回基本报告结构 # 先返回基本报告结构
base_report = dict(report) base_report = dict(report)
base_report['ai_analysis'] = '' base_report['ai_analysis'] = ''
@@ -509,21 +578,21 @@ class StockAnalyzer:
yield base_report_json yield base_report_json
# 然后流式返回AI分析部分 # 然后流式返回AI分析部分
logger.debug(f"开始获取股票 {stock_code} 的流式AI分析") logger.debug(f"开始获取 {stock_code} 的流式AI分析")
ai_chunks_count = 0 ai_chunks_count = 0
for ai_chunk in self.get_ai_analysis(df, stock_code, stream=True): for ai_chunk in self.get_ai_analysis(df, stock_code, market_type, stream=True):
ai_chunks_count += 1 ai_chunks_count += 1
yield ai_chunk yield ai_chunk
logger.info(f"股票 {stock_code} 流式AI分析完成共发送 {ai_chunks_count} 个块") logger.info(f" {stock_code} 流式AI分析完成共发送 {ai_chunks_count} 个块")
else: else:
logger.info(f"以非流式模式返回股票 {stock_code} 分析结果") logger.info(f"以非流式模式返回 {stock_code} 分析结果")
logger.debug(f"开始获取股票 {stock_code} 的AI分析") logger.debug(f"开始获取 {stock_code} 的AI分析")
report['ai_analysis'] = self.get_ai_analysis(df, stock_code) report['ai_analysis'] = self.get_ai_analysis(df, stock_code, market_type)
logger.debug(f"AI分析结果长度: {len(report['ai_analysis'])}") logger.debug(f"AI分析结果长度: {len(report['ai_analysis'])}")
return report return report
except Exception as e: except Exception as e:
error_msg = f"分析股票 {stock_code} 时出错: {str(e)}" error_msg = f"分析 {stock_code} 时出错: {str(e)}"
logger.error(error_msg) logger.error(error_msg)
logger.exception(e) logger.exception(e)
@@ -535,23 +604,23 @@ class StockAnalyzer:
raise raise
def scan_market(self, stock_list, min_score=60, market_type='A', stream=False): def scan_market(self, stock_list, min_score=60, market_type='A', stream=False):
"""扫描市场,寻找符合条件的股票""" """扫描市场,寻找符合条件的"""
logger.info(f"开始扫描市场,股票数量: {len(stock_list)}, 最低分数: {min_score}, 市场: {market_type}, 流式模式: {stream}") logger.info(f"开始扫描市场,数量: {len(stock_list)}, 最低分数: {min_score}, 市场: {market_type}, 流式模式: {stream}")
if not stream: if not stream:
recommendations = [] recommendations = []
for stock_code in stock_list: for stock_code in stock_list:
try: try:
logger.debug(f"分析股票: {stock_code}") logger.debug(f"分析: {stock_code}")
report = self.analyze_stock(stock_code, market_type) report = self.analyze_stock(stock_code, market_type)
if report['score'] >= min_score: if report['score'] >= min_score:
logger.info(f"股票 {stock_code} 评分 {report['score']} >= {min_score},添加到推荐列表") logger.info(f" {stock_code} 评分 {report['score']} >= {min_score},添加到推荐列表")
recommendations.append(report) recommendations.append(report)
else: else:
logger.debug(f"股票 {stock_code} 评分 {report['score']} < {min_score},不添加到推荐列表") logger.debug(f" {stock_code} 评分 {report['score']} < {min_score},不添加到推荐列表")
except Exception as e: except Exception as e:
logger.error(f"分析股票 {stock_code} 时出错: {str(e)}") logger.error(f"分析 {stock_code} 时出错: {str(e)}")
logger.exception(e) logger.exception(e)
continue continue
@@ -565,16 +634,16 @@ class StockAnalyzer:
stock_count = 0 stock_count = 0
for stock_code in stock_list: for stock_code in stock_list:
stock_count += 1 stock_count += 1
logger.debug(f"流式分析股票 {stock_code} ({stock_count}/{len(stock_list)})") logger.debug(f"流式分析 {stock_code} ({stock_count}/{len(stock_list)})")
try: try:
# 分析单只股票并获取流式结果 # 分析单只股票并获取流式结果
chunk_count = 0 chunk_count = 0
for chunk in self.analyze_stock(stock_code, market_type, stream=True): for chunk in self.analyze_stock(stock_code, market_type, stream=True):
chunk_count += 1 chunk_count += 1
yield chunk yield chunk
logger.debug(f"股票 {stock_code} 流式分析完成,共 {chunk_count} 个块") logger.debug(f" {stock_code} 流式分析完成,共 {chunk_count} 个块")
except Exception as e: except Exception as e:
error_msg = f"分析股票 {stock_code} 时出错: {str(e)}" error_msg = f"分析 {stock_code} 时出错: {str(e)}"
logger.error(error_msg) logger.error(error_msg)
logger.exception(e) logger.exception(e)
error_json = json.dumps({'stock_code': stock_code, 'error': error_msg}) error_json = json.dumps({'stock_code': stock_code, 'error': error_msg})

View File

@@ -301,29 +301,28 @@
<option value="A">A股</option> <option value="A">A股</option>
<option value="HK">港股</option> <option value="HK">港股</option>
<option value="US">美股</option> <option value="US">美股</option>
<option value="ETF">ETF基金</option>
<option value="LOF">LOF基金</option>
</select> </select>
</div> </div>
<!-- 美股搜索框 --> <!-- 搜索框 -->
<div id="usStockSearch" class="mb-4 hidden"> <div id="searchContainer" class="mb-4 hidden">
<div class="relative"> <div class="relative">
<input type="text" <input type="text"
id="searchInput" id="searchInput"
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="输入美股名称搜索(中文和英文都试下)" placeholder="输入名称回车搜索"
oninput="handleSearchInput(event)"> onkeydown="handleKeyDown(event)">
<!-- 添加搜索 loading 图标 -->
<div id="searchLoading" class="absolute right-3 top-2.5 hidden"> <div id="searchLoading" class="absolute right-3 top-2.5 hidden">
<svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
</div> </div>
<!-- 搜索结果下拉框 --> <div id="searchResults" class="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg hidden max-h-80 overflow-y-auto">
<div id="searchResults" </div>
class="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg hidden">
</div> </div>
<!-- 添加错误提示 -->
<div id="searchError" class="hidden absolute z-10 w-full mt-1 p-3 bg-red-50 text-red-600 rounded-md border border-red-200"> <div id="searchError" class="hidden absolute z-10 w-full mt-1 p-3 bg-red-50 text-red-600 rounded-md border border-red-200">
</div> </div>
</div> </div>
@@ -342,6 +341,17 @@
}; };
} }
function handleKeyDown(event) {
// 检查是否按下回车键且不在输入法编辑状态
if (event.key === 'Enter' && !event.isComposing) {
event.preventDefault(); // 阻止默认行为
const keyword = event.target.value.trim();
if (keyword) {
debouncedSearch(keyword);
}
}
}
// 使用防抖包装搜索函数 // 使用防抖包装搜索函数
const debouncedSearch = debounce(async (keyword) => { const debouncedSearch = debounce(async (keyword) => {
if (!keyword) { if (!keyword) {
@@ -350,20 +360,26 @@
return; return;
} }
// 显示 loading const marketType = document.getElementById('marketType').value;
document.getElementById('searchLoading').classList.remove('hidden'); document.getElementById('searchLoading').classList.remove('hidden');
// 隐藏之前的错误信息
document.getElementById('searchError').classList.add('hidden'); document.getElementById('searchError').classList.add('hidden');
try { try {
const response = await fetch(`/search_us_stocks?keyword=${encodeURIComponent(keyword)}`); let endpoint = '';
if (marketType === 'US') {
endpoint = '/search_us_stocks';
} else if (['ETF', 'LOF'].includes(marketType)) {
endpoint = '/search_funds';
}
const response = await fetch(`${endpoint}?market_type=${marketType}&keyword=${encodeURIComponent(keyword)}`);
const data = await response.json(); const data = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(data.error || '搜索失败'); throw new Error(data.error || '搜索失败');
} }
displaySearchResults(data.results); displaySearchResults(data.results, marketType);
} catch (error) { } catch (error) {
console.error('搜索出错:', error); console.error('搜索出错:', error);
const errorDiv = document.getElementById('searchError'); const errorDiv = document.getElementById('searchError');
@@ -373,12 +389,8 @@
} finally { } finally {
document.getElementById('searchLoading').classList.add('hidden'); document.getElementById('searchLoading').classList.add('hidden');
} }
}, 500); // 设置500ms的防抖延迟 }, 500);
// 修改输入事件处理函数
function handleSearchInput(event) {
debouncedSearch(event.target.value);
}
</script> </script>
<div class="mb-4"> <div class="mb-4">
<label for="batchStocks" class="block text-sm font-medium text-gray-700 mb-2"> <label for="batchStocks" class="block text-sm font-medium text-gray-700 mb-2">
@@ -395,12 +407,28 @@
function handleMarketTypeChange() { function handleMarketTypeChange() {
const marketType = document.getElementById('marketType').value; const marketType = document.getElementById('marketType').value;
const searchDiv = document.getElementById('usStockSearch'); const searchContainer = document.getElementById('searchContainer');
searchDiv.classList.toggle('hidden', marketType !== 'US'); const searchInput = document.getElementById('searchInput');
const batchStocks = document.getElementById('batchStocks');
// 清空搜索结果 // 显示/隐藏搜索框
searchContainer.classList.toggle('hidden', !['US', 'ETF', 'LOF'].includes(marketType));
// 更新搜索框提示文本
if (marketType === 'US') {
searchInput.placeholder = '输入美股名称回车搜索(中文和英文都试下)';
} else if (marketType === 'ETF') {
searchInput.placeholder = '输入ETF基金名称回车搜索';
} else if (marketType === 'LOF') {
searchInput.placeholder = '输入LOF基金名称回车搜索';
}
// 清空搜索结果和输入
document.getElementById('searchResults').innerHTML = ''; document.getElementById('searchResults').innerHTML = '';
document.getElementById('searchInput').value = ''; searchInput.value = '';
// 清空代码输入框
batchStocks.value = '';
} }
function debounceSearch(event) { function debounceSearch(event) {
@@ -410,7 +438,7 @@
}, 300); }, 300);
} }
function displaySearchResults(results) { function displaySearchResults(results, marketType) {
const resultsDiv = document.getElementById('searchResults'); const resultsDiv = document.getElementById('searchResults');
if (!results || results.length === 0) { if (!results || results.length === 0) {
@@ -418,18 +446,32 @@
return; return;
} }
let html = '<div class="py-1">'; let html = '<div class="divide-y divide-gray-100">'; // 添加分割线
results.forEach(stock => { results.forEach(item => {
let rightContent = '';
if (marketType === 'US') {
rightContent = `
<div class="font-medium">$${item.price}</div>
<div class="text-sm text-gray-500">市值: ${formatMarketValue(item.market_value)}</div>
`;
} else {
rightContent = `
<div class="font-medium">¥${item.price}</div>
<div class="text-sm text-gray-500">
${item.discount_rate ? `折价率: ${item.discount_rate}%` : ''}
</div>
`;
}
html += ` html += `
<div class="px-4 py-2 hover:bg-gray-100 cursor-pointer flex justify-between items-center" <div class="px-4 py-2 hover:bg-gray-100 cursor-pointer flex justify-between items-center"
onclick="selectStock('${stock.symbol}')"> onclick="selectStock('${item.symbol}')">
<div> <div>
<div class="font-medium">${stock.name}</div> <div class="font-medium">${item.name}</div>
<div class="text-sm text-gray-500">${stock.symbol}</div> <div class="text-sm text-gray-500">${item.symbol}</div>
</div> </div>
<div class="text-right"> <div class="text-right">
<div class="font-medium">$${stock.price}</div> ${rightContent}
<div class="text-sm text-gray-500">市值: ${formatMarketValue(stock.market_value)}</div>
</div> </div>
</div> </div>
`; `;
@@ -686,10 +728,10 @@
errorCard = document.createElement('div'); errorCard = document.createElement('div');
errorCard.id = `error-${stockCode}`; errorCard.id = `error-${stockCode}`;
errorCard.className = 'bg-red-50 p-4 rounded-lg text-red-600'; errorCard.className = 'bg-red-50 p-4 rounded-lg text-red-600';
errorCard.innerHTML = `分析股票 ${stockCode} 出错: ${chunk.error}`; errorCard.innerHTML = `分析 ${stockCode} 出错: ${chunk.error}`;
container.appendChild(errorCard); container.appendChild(errorCard);
} else { } else {
errorCard.innerHTML = `分析股票 ${stockCode} 出错: ${chunk.error}`; errorCard.innerHTML = `分析 ${stockCode} 出错: ${chunk.error}`;
} }
return; return;
} }

View File

@@ -1,6 +1,7 @@
from flask import Flask, render_template, request, jsonify, Response, stream_with_context from flask import Flask, render_template, request, jsonify, Response, stream_with_context
from stock_analyzer import StockAnalyzer from stock_analyzer import StockAnalyzer
from us_stock_service import USStockService from us_stock_service import USStockService
from fund_service import FundService # 新增导入
import threading import threading
import os import os
import traceback import traceback
@@ -18,6 +19,7 @@ logger = get_logger()
app = Flask(__name__) app = Flask(__name__)
analyzer = StockAnalyzer() analyzer = StockAnalyzer()
us_stock_service = USStockService() us_stock_service = USStockService()
fund_service = FundService() # 新增服务实例
@app.route('/') @app.route('/')
def index(): def index():
@@ -102,7 +104,7 @@ def analyze():
return Response(stream_with_context(generate()), mimetype='application/json') return Response(stream_with_context(generate()), mimetype='application/json')
except Exception as e: except Exception as e:
error_msg = f"分析股票时出错: {str(e)}" error_msg = f"分析时出错: {str(e)}"
logger.error(error_msg) logger.error(error_msg)
logger.exception(e) logger.exception(e)
return jsonify({'error': error_msg}), 500 return jsonify({'error': error_msg}), 500
@@ -121,6 +123,22 @@ def search_us_stocks():
print(f"搜索美股代码时出错: {str(e)}") print(f"搜索美股代码时出错: {str(e)}")
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# 添加基金搜索路由
@app.route('/search_funds', methods=['GET'])
def search_funds():
try:
keyword = request.args.get('keyword', '')
market_type = request.args.get('market_type', '')
if not keyword:
return jsonify({'error': '请输入搜索关键词'}), 400
results = fund_service.search_funds(keyword, market_type)
return jsonify({'results': results})
except Exception as e:
logger.error(f"搜索基金代码时出错: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/test_api_connection', methods=['POST']) @app.route('/test_api_connection', methods=['POST'])
def test_api_connection(): def test_api_connection():
"""测试API连接""" """测试API连接"""