ADD: 支持ETF、 LOF基金!
This commit is contained in:
51
fund_service.py
Normal file
51
fund_service.py
Normal 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)}")
|
||||
@@ -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
|
||||
|
||||
if start_date is None:
|
||||
@@ -55,7 +55,6 @@ class StockAnalyzer:
|
||||
end_date=end_date,
|
||||
adjust="qfq"
|
||||
)
|
||||
# A股数据列名映射
|
||||
elif market_type == 'HK':
|
||||
df = ak.stock_hk_daily(
|
||||
symbol=stock_code,
|
||||
@@ -68,10 +67,22 @@ class StockAnalyzer:
|
||||
end_date=end_date,
|
||||
adjust="qfq"
|
||||
)
|
||||
# elif market_type == 'CRYPTO':
|
||||
# df = ak.crypto_js_spot(
|
||||
# symbol=stock_code
|
||||
# )
|
||||
elif market_type == 'ETF':
|
||||
df = ak.fund_etf_hist_em(
|
||||
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:
|
||||
raise ValueError(f"不支持的市场类型: {market_type}")
|
||||
|
||||
@@ -214,10 +225,10 @@ class StockAnalyzer:
|
||||
print(f"计算评分时出错: {str(e)}")
|
||||
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 分析"""
|
||||
try:
|
||||
logger.info(f"开始AI分析股票 {stock_code}, 流式模式: {stream}")
|
||||
logger.info(f"开始AI分析 {stock_code}, 流式模式: {stream}")
|
||||
recent_data = df.tail(14).to_dict('records')
|
||||
|
||||
technical_summary = {
|
||||
@@ -227,8 +238,70 @@ class StockAnalyzer:
|
||||
'rsi_level': df.iloc[-1]['RSI']
|
||||
}
|
||||
|
||||
# 根据市场类型调整分析提示
|
||||
if market_type in ['ETF', 'LOF']:
|
||||
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}
|
||||
@@ -244,7 +317,7 @@ class StockAnalyzer:
|
||||
5. 关键技术位分析
|
||||
6. 具体交易建议(包含止损位)
|
||||
|
||||
请基于技术指标和市场动态进行分析,给出具体数据支持。
|
||||
请基于技术指标和A股市场特点进行分析,给出具体数据支持。
|
||||
"""
|
||||
|
||||
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]:
|
||||
"""处理AI流式响应"""
|
||||
logger.info(f"开始处理股票 {stock_code} 的AI流式响应")
|
||||
logger.info(f"开始处理 {stock_code} 的AI流式响应")
|
||||
buffer = ""
|
||||
chunk_count = 0
|
||||
|
||||
@@ -457,49 +530,45 @@ class StockAnalyzer:
|
||||
return '强烈建议卖出'
|
||||
|
||||
def analyze_stock(self, stock_code, market_type='A', stream=False):
|
||||
"""分析单个股票"""
|
||||
"""分析股票或基金"""
|
||||
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)
|
||||
|
||||
# 计算技术指标
|
||||
logger.debug(f"计算股票 {stock_code} 技术指标")
|
||||
logger.debug(f"计算 {stock_code} 技术指标")
|
||||
df = self.calculate_indicators(df)
|
||||
|
||||
# 评分系统
|
||||
logger.debug(f"计算股票 {stock_code} 评分")
|
||||
logger.debug(f"计算 {stock_code} 评分")
|
||||
score = self.calculate_score(df)
|
||||
logger.info(f"股票 {stock_code} 评分结果: {score}")
|
||||
logger.info(f"{stock_code} 评分结果: {score}")
|
||||
|
||||
# 获取最新数据
|
||||
latest = df.iloc[-1]
|
||||
prev = df.iloc[-2]
|
||||
|
||||
# 处理 RSI 的 NaN 值
|
||||
rsi_value = latest['RSI']
|
||||
if pd.isna(rsi_value):
|
||||
rsi_value = None
|
||||
|
||||
# 生成报告(保持原有格式)
|
||||
# 生成报告
|
||||
report = {
|
||||
'stock_code': stock_code,
|
||||
'market_type': market_type, # 添加市场类型
|
||||
'analysis_date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'score': score,
|
||||
'price': latest['close'],
|
||||
'price_change': (latest['close'] - prev['close']) / prev['close'] * 100,
|
||||
'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',
|
||||
'volume_status': 'HIGH' if latest['Volume_Ratio'] > 1.5 else 'NORMAL',
|
||||
'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:
|
||||
logger.info(f"以流式模式返回股票 {stock_code} 分析结果")
|
||||
logger.info(f"以流式模式返回 {stock_code} 分析结果")
|
||||
# 先返回基本报告结构
|
||||
base_report = dict(report)
|
||||
base_report['ai_analysis'] = ''
|
||||
@@ -509,21 +578,21 @@ class StockAnalyzer:
|
||||
yield base_report_json
|
||||
|
||||
# 然后流式返回AI分析部分
|
||||
logger.debug(f"开始获取股票 {stock_code} 的流式AI分析")
|
||||
logger.debug(f"开始获取 {stock_code} 的流式AI分析")
|
||||
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
|
||||
yield ai_chunk
|
||||
logger.info(f"股票 {stock_code} 流式AI分析完成,共发送 {ai_chunks_count} 个块")
|
||||
logger.info(f" {stock_code} 流式AI分析完成,共发送 {ai_chunks_count} 个块")
|
||||
else:
|
||||
logger.info(f"以非流式模式返回股票 {stock_code} 分析结果")
|
||||
logger.debug(f"开始获取股票 {stock_code} 的AI分析")
|
||||
report['ai_analysis'] = self.get_ai_analysis(df, stock_code)
|
||||
logger.info(f"以非流式模式返回 {stock_code} 分析结果")
|
||||
logger.debug(f"开始获取 {stock_code} 的AI分析")
|
||||
report['ai_analysis'] = self.get_ai_analysis(df, stock_code, market_type)
|
||||
logger.debug(f"AI分析结果长度: {len(report['ai_analysis'])}")
|
||||
return report
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"分析股票 {stock_code} 时出错: {str(e)}"
|
||||
error_msg = f"分析 {stock_code} 时出错: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
logger.exception(e)
|
||||
|
||||
@@ -535,23 +604,23 @@ class StockAnalyzer:
|
||||
raise
|
||||
|
||||
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:
|
||||
recommendations = []
|
||||
|
||||
for stock_code in stock_list:
|
||||
try:
|
||||
logger.debug(f"分析股票: {stock_code}")
|
||||
logger.debug(f"分析: {stock_code}")
|
||||
report = self.analyze_stock(stock_code, market_type)
|
||||
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)
|
||||
else:
|
||||
logger.debug(f"股票 {stock_code} 评分 {report['score']} < {min_score},不添加到推荐列表")
|
||||
logger.debug(f" {stock_code} 评分 {report['score']} < {min_score},不添加到推荐列表")
|
||||
except Exception as e:
|
||||
logger.error(f"分析股票 {stock_code} 时出错: {str(e)}")
|
||||
logger.error(f"分析 {stock_code} 时出错: {str(e)}")
|
||||
logger.exception(e)
|
||||
continue
|
||||
|
||||
@@ -565,16 +634,16 @@ class StockAnalyzer:
|
||||
stock_count = 0
|
||||
for stock_code in stock_list:
|
||||
stock_count += 1
|
||||
logger.debug(f"流式分析股票 {stock_code} ({stock_count}/{len(stock_list)})")
|
||||
logger.debug(f"流式分析 {stock_code} ({stock_count}/{len(stock_list)})")
|
||||
try:
|
||||
# 分析单只股票并获取流式结果
|
||||
chunk_count = 0
|
||||
for chunk in self.analyze_stock(stock_code, market_type, stream=True):
|
||||
chunk_count += 1
|
||||
yield chunk
|
||||
logger.debug(f"股票 {stock_code} 流式分析完成,共 {chunk_count} 个块")
|
||||
logger.debug(f" {stock_code} 流式分析完成,共 {chunk_count} 个块")
|
||||
except Exception as e:
|
||||
error_msg = f"分析股票 {stock_code} 时出错: {str(e)}"
|
||||
error_msg = f"分析 {stock_code} 时出错: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
logger.exception(e)
|
||||
error_json = json.dumps({'stock_code': stock_code, 'error': error_msg})
|
||||
|
||||
@@ -301,29 +301,28 @@
|
||||
<option value="A">A股</option>
|
||||
<option value="HK">港股</option>
|
||||
<option value="US">美股</option>
|
||||
<option value="ETF">ETF基金</option>
|
||||
<option value="LOF">LOF基金</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 美股搜索框 -->
|
||||
<div id="usStockSearch" class="mb-4 hidden">
|
||||
<!-- 搜索框 -->
|
||||
<div id="searchContainer" class="mb-4 hidden">
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
id="searchInput"
|
||||
class="w-full p-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="输入美股名称搜索(中文和英文都试下)"
|
||||
oninput="handleSearchInput(event)">
|
||||
<!-- 添加搜索 loading 图标 -->
|
||||
placeholder="输入名称回车搜索"
|
||||
onkeydown="handleKeyDown(event)">
|
||||
<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">
|
||||
<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>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 搜索结果下拉框 -->
|
||||
<div id="searchResults"
|
||||
class="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg hidden">
|
||||
<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>
|
||||
</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>
|
||||
</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) => {
|
||||
if (!keyword) {
|
||||
@@ -350,20 +360,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示 loading
|
||||
const marketType = document.getElementById('marketType').value;
|
||||
document.getElementById('searchLoading').classList.remove('hidden');
|
||||
// 隐藏之前的错误信息
|
||||
document.getElementById('searchError').classList.add('hidden');
|
||||
|
||||
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();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '搜索失败');
|
||||
}
|
||||
|
||||
displaySearchResults(data.results);
|
||||
displaySearchResults(data.results, marketType);
|
||||
} catch (error) {
|
||||
console.error('搜索出错:', error);
|
||||
const errorDiv = document.getElementById('searchError');
|
||||
@@ -373,12 +389,8 @@
|
||||
} finally {
|
||||
document.getElementById('searchLoading').classList.add('hidden');
|
||||
}
|
||||
}, 500); // 设置500ms的防抖延迟
|
||||
}, 500);
|
||||
|
||||
// 修改输入事件处理函数
|
||||
function handleSearchInput(event) {
|
||||
debouncedSearch(event.target.value);
|
||||
}
|
||||
</script>
|
||||
<div class="mb-4">
|
||||
<label for="batchStocks" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -395,12 +407,28 @@
|
||||
|
||||
function handleMarketTypeChange() {
|
||||
const marketType = document.getElementById('marketType').value;
|
||||
const searchDiv = document.getElementById('usStockSearch');
|
||||
searchDiv.classList.toggle('hidden', marketType !== 'US');
|
||||
const searchContainer = document.getElementById('searchContainer');
|
||||
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('searchInput').value = '';
|
||||
searchInput.value = '';
|
||||
|
||||
// 清空代码输入框
|
||||
batchStocks.value = '';
|
||||
}
|
||||
|
||||
function debounceSearch(event) {
|
||||
@@ -410,7 +438,7 @@
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function displaySearchResults(results) {
|
||||
function displaySearchResults(results, marketType) {
|
||||
const resultsDiv = document.getElementById('searchResults');
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
@@ -418,18 +446,32 @@
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="py-1">';
|
||||
results.forEach(stock => {
|
||||
let html = '<div class="divide-y divide-gray-100">'; // 添加分割线
|
||||
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 += `
|
||||
<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 class="font-medium">${stock.name}</div>
|
||||
<div class="text-sm text-gray-500">${stock.symbol}</div>
|
||||
<div class="font-medium">${item.name}</div>
|
||||
<div class="text-sm text-gray-500">${item.symbol}</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="font-medium">$${stock.price}</div>
|
||||
<div class="text-sm text-gray-500">市值: ${formatMarketValue(stock.market_value)}</div>
|
||||
${rightContent}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -686,10 +728,10 @@
|
||||
errorCard = document.createElement('div');
|
||||
errorCard.id = `error-${stockCode}`;
|
||||
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);
|
||||
} else {
|
||||
errorCard.innerHTML = `分析股票 ${stockCode} 出错: ${chunk.error}`;
|
||||
errorCard.innerHTML = `分析 ${stockCode} 出错: ${chunk.error}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask import Flask, render_template, request, jsonify, Response, stream_with_context
|
||||
from stock_analyzer import StockAnalyzer
|
||||
from us_stock_service import USStockService
|
||||
from fund_service import FundService # 新增导入
|
||||
import threading
|
||||
import os
|
||||
import traceback
|
||||
@@ -18,6 +19,7 @@ logger = get_logger()
|
||||
app = Flask(__name__)
|
||||
analyzer = StockAnalyzer()
|
||||
us_stock_service = USStockService()
|
||||
fund_service = FundService() # 新增服务实例
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
@@ -102,7 +104,7 @@ def analyze():
|
||||
return Response(stream_with_context(generate()), mimetype='application/json')
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"分析股票时出错: {str(e)}"
|
||||
error_msg = f"分析时出错: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
logger.exception(e)
|
||||
return jsonify({'error': error_msg}), 500
|
||||
@@ -121,6 +123,22 @@ def search_us_stocks():
|
||||
print(f"搜索美股代码时出错: {str(e)}")
|
||||
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'])
|
||||
def test_api_connection():
|
||||
"""测试API连接"""
|
||||
|
||||
Reference in New Issue
Block a user