first commit

This commit is contained in:
aaron 2025-12-28 10:12:30 +08:00
commit 8c6ae691ef
29 changed files with 4962 additions and 0 deletions

View File

@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)"
],
"deny": [],
"ask": []
}
}

9
.env.example Normal file
View File

@ -0,0 +1,9 @@
TUSHARE_TOKEN=0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
DATABASE_URL=sqlite:///stockagent.db
API_HOST=0.0.0.0
API_PORT=8000
CACHE_EXPIRE=3600
LOG_LEVEL=INFO

40
.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
.env
.venv
env/
venv/
*.log
logs/
.pytest_cache/
.coverage
htmlcov/
.DS_Store
*.sqlite
*.db
.idea/
.vscode/

146
README.md Normal file
View File

@ -0,0 +1,146 @@
# 股票分析筛选 AI Agent
基于 tushare 数据接口的智能股票筛选和分析系统,支持多种投资策略的量化评分。
## 功能特性
- **多策略筛选**: 价值投资、成长投资、技术分析三大策略
- **实时数据**: 基于 tushare API 获取最新股票数据
- **智能评分**: AI 算法对股票进行综合评分和排名
- **可视化分析**: 直观的图表展示和财务指标雷达图
- **缓存优化**: Redis 缓存提升数据查询性能
## 技术栈
- **后端**: FastAPI + Python 3.9+
- **数据源**: Tushare API
- **数据处理**: pandas, numpy, scikit-learn
- **缓存**: Redis
- **数据库**: SQLAlchemy (支持 SQLite/PostgreSQL)
- **前端**: Streamlit
- **可视化**: Plotly
## 快速开始
### 1. 环境配置
```bash
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 安装依赖
pip install -r requirements.txt
# 配置环境变量
cp .env.example .env
# 编辑 .env 文件,设置你的 tushare token
```
### 2. 配置 tushare token
`.env` 文件中设置:
```
TUSHARE_TOKEN=your_tushare_token_here
```
### 3. 启动服务
```bash
# 启动 API 服务
python main.py
# 启动 Web 界面 (新终端)
streamlit run src/web/streamlit_app.py
```
### 4. 访问应用
- API 文档: http://localhost:8000/docs
- Web 界面: http://localhost:8501
## 投资策略说明
### 价值投资策略
- 关注指标: PE、PB、ROE、负债率、净利率
- 适合: 寻找被低估的优质股票
- 评分权重: PE(25%) + PB(20%) + ROE(20%) + 负债(15%) + 其他(20%)
### 成长投资策略
- 关注指标: 营收增长、利润增长、ROE、研发投入
- 适合: 寻找高成长潜力的股票
- 评分权重: 营收增长(30%) + 利润增长(25%) + ROE(20%) + 其他(25%)
### 技术分析策略
- 关注指标: 移动平均线、RSI、MACD、成交量、波林带
- 适合: 短期交易和趋势跟踪
- 评分权重: 趋势(30%) + 动量(25%) + 成交量(20%) + 其他(25%)
## API 接口
### 获取股票列表
```http
GET /api/stocks
```
### 单股分析
```http
POST /api/analyze
Content-Type: application/json
{
"ts_code": "000001.SZ",
"strategy": "value"
}
```
### 批量筛选
```http
POST /api/screen
Content-Type: application/json
{
"strategy": "value",
"min_score": 60,
"limit": 50
}
```
## 项目结构
```
StockAgent/
├── src/
│ ├── data/ # 数据获取和存储
│ ├── analysis/ # 财务和技术指标计算
│ ├── strategies/ # 投资策略实现
│ ├── utils/ # 工具函数
│ └── web/ # Web 界面
├── config/ # 配置文件
├── tests/ # 测试文件
├── main.py # API 主程序
└── requirements.txt # 依赖包
```
## 使用示例
```python
from src.data import TushareClient
from src.strategies import ValueStrategy
# 初始化数据客户端
client = TushareClient()
# 获取股票数据
stocks = client.get_stock_list()
financial_data = client.get_financial_data('000001.SZ')
# 使用价值投资策略评分
strategy = ValueStrategy()
score = strategy.calculate_score(stock_data)
print(f"投资评分: {score}")
```
## 许可证
MIT License

19
config/config.py Normal file
View File

@ -0,0 +1,19 @@
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
TUSHARE_TOKEN = os.getenv('TUSHARE_TOKEN')
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
REDIS_DB = int(os.getenv('REDIS_DB', 0))
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///stockagent.db')
API_HOST = os.getenv('API_HOST', '0.0.0.0')
API_PORT = int(os.getenv('API_PORT', 8000))
CACHE_EXPIRE = int(os.getenv('CACHE_EXPIRE', 3600))
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')

333
main.py Normal file
View File

@ -0,0 +1,333 @@
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Dict, Optional
import uvicorn
import json
import numpy as np
from src.data import TushareClient, Database, RedisCache
from src.analysis import FinancialIndicators, TechnicalIndicators
from src.strategies import ValueStrategy, GrowthStrategy, TechnicalStrategy
from src.utils import setup_logger, validate_ts_code
from config.config import Config
def format_stock_code(code: str) -> str:
"""格式化股票代码,自动添加交易所后缀"""
if not code:
return code
# 移除所有空格并转为大写
clean_code = code.strip().replace(' ', '').upper()
# 如果已经包含交易所后缀,直接返回
if '.SZ' in clean_code or '.SH' in clean_code:
return clean_code
# 只保留数字部分
numeric_code = ''.join(filter(str.isdigit, clean_code))
if len(numeric_code) == 6:
# 深交所000xxx(主板), 002xxx(中小板), 300xxx(创业板)
if numeric_code.startswith('000') or numeric_code.startswith('002') or numeric_code.startswith('300'):
return numeric_code + '.SZ'
# 上交所600xxx, 601xxx, 603xxx(主板), 688xxx(科创板)
elif numeric_code.startswith('60') or numeric_code.startswith('688'):
return numeric_code + '.SH'
# 默认返回原始代码加.SZ
return numeric_code + '.SZ' if numeric_code else code
def clean_data_for_json(data):
"""清理数据确保JSON兼容性"""
if isinstance(data, dict):
return {k: clean_data_for_json(v) for k, v in data.items()}
elif isinstance(data, list):
return [clean_data_for_json(item) for item in data]
elif isinstance(data, (int, str, bool)) or data is None:
return data
elif isinstance(data, float):
if np.isnan(data) or np.isinf(data):
return 0
return data
else:
return str(data)
app = FastAPI(title="Stock Agent API", description="优质股票分析筛选 AI Agent", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
logger = setup_logger(__name__)
tushare_client = TushareClient()
database = Database()
cache = RedisCache()
class AnalysisRequest(BaseModel):
ts_code: str
strategy: str = "value"
class ScreenRequest(BaseModel):
strategy: str = "value"
min_score: float = 60
limit: int = 50
@app.get("/")
async def root():
return {"message": "Stock Agent API is running"}
@app.get("/api/stocks")
async def get_stock_list():
try:
cache_key = "stock_list"
cached_data = cache.get(cache_key)
if cached_data:
return cached_data
stocks = tushare_client.get_stock_list()
if stocks.empty:
raise HTTPException(status_code=404, detail="No stock data found")
stock_list = stocks.to_dict('records')
cache.set(cache_key, stock_list, 3600)
return {"data": stock_list, "count": len(stock_list)}
except Exception as e:
logger.error(f"Error getting stock list: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/analyze")
async def analyze_stock(request: AnalysisRequest):
try:
# 格式化股票代码
formatted_ts_code = format_stock_code(request.ts_code)
if not validate_ts_code(formatted_ts_code):
raise HTTPException(status_code=400, detail="Invalid stock code")
cache_key = f"analysis_{formatted_ts_code}_{request.strategy}"
cached_data = cache.get(cache_key)
if cached_data:
return cached_data
# 获取股票基本信息
stock_basic_info = tushare_client.get_stock_basic(formatted_ts_code)
if not stock_basic_info:
raise HTTPException(status_code=404, detail="Stock not found")
daily_data = tushare_client.get_stock_daily(formatted_ts_code)
if daily_data.empty:
raise HTTPException(status_code=404, detail="No daily data found")
financial_data = tushare_client.get_financial_data(formatted_ts_code)
financial_ratios = FinancialIndicators.calculate_all_ratios(financial_data)
daily_with_indicators = TechnicalIndicators.calculate_all_indicators(daily_data)
stock_data = {
'ts_code': formatted_ts_code,
'financial_ratios': financial_ratios,
'technical_indicators': daily_with_indicators.iloc[-1].to_dict() if not daily_with_indicators.empty else {}
}
if request.strategy == "value":
strategy = ValueStrategy()
elif request.strategy == "growth":
strategy = GrowthStrategy()
elif request.strategy == "technical":
strategy = TechnicalStrategy()
else:
raise HTTPException(status_code=400, detail="Invalid strategy")
score = strategy.calculate_score(stock_data)
# 如果是技术策略,添加交易信号分析
trading_signals = None
if request.strategy == "technical":
trading_signals = strategy.get_trading_signals(stock_data)
result = {
'ts_code': request.ts_code,
'strategy': request.strategy,
'score': round(score, 2),
'financial_ratios': clean_data_for_json(financial_ratios),
'recommendation': 'BUY' if score >= 65 else 'HOLD' if score >= 45 else 'SELL',
# 添加股票基本信息
'name': stock_basic_info['name'],
'industry': stock_basic_info['industry'],
'current_price': clean_data_for_json(stock_basic_info['current_price']),
'market_cap': clean_data_for_json(stock_basic_info['market_cap']),
'list_date': stock_basic_info['list_date']
}
# 添加交易信号(如果是技术分析)
if trading_signals:
result['trading_signals'] = clean_data_for_json(trading_signals)
# 确保整个结果都是JSON兼容的
result = clean_data_for_json(result)
cache.set(cache_key, result, 1800)
return result
except Exception as e:
logger.error(f"Error analyzing stock {request.ts_code}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/screen")
async def screen_stocks(request: ScreenRequest):
import uuid
request_id = str(uuid.uuid4())[:8]
try:
logger.info(f"[{request_id}] Starting screening request with strategy={request.strategy}")
cache_key = f"screen_hs300_{request.strategy}_{request.min_score}_{request.limit}"
cached_data = cache.get(cache_key)
if cached_data:
logger.info(f"[{request_id}] Returning cached data")
return cached_data
# 获取沪深300成分股
hs300_stocks = tushare_client.get_hs300_stocks()
if hs300_stocks.empty:
raise HTTPException(status_code=404, detail="No HS300 stock data found")
# 限制处理数量避免超时先处理前300只进行测试
max_process = min(300, len(hs300_stocks))
hs300_stocks = hs300_stocks.head(max_process)
logger.info(f"Limited processing to {max_process} stocks to avoid timeout")
# 初始化三种策略
value_strategy = ValueStrategy()
growth_strategy = GrowthStrategy()
technical_strategy = TechnicalStrategy()
results = []
processed_count = 0
if request.strategy == "comprehensive":
logger.info(f"[{request_id}] Starting comprehensive screening of {len(hs300_stocks)} HS300 stocks")
else:
# 保持向后兼容,支持单一策略
if request.strategy == "value":
strategy = value_strategy
elif request.strategy == "growth":
strategy = growth_strategy
elif request.strategy == "technical":
strategy = technical_strategy
else:
raise HTTPException(status_code=400, detail="Invalid strategy")
logger.info(f"[{request_id}] Starting to screen {len(hs300_stocks)} HS300 stocks with {request.strategy} strategy")
# 处理所有沪深300成分股
for _, stock in hs300_stocks.iterrows():
try:
ts_code = stock['ts_code']
logger.debug(f"Processing stock: {ts_code}") # 添加调试日志
financial_data = tushare_client.get_financial_data(ts_code)
financial_ratios = FinancialIndicators.calculate_all_ratios(financial_data)
daily_data = tushare_client.get_stock_daily(ts_code)
if not daily_data.empty:
daily_with_indicators = TechnicalIndicators.calculate_all_indicators(daily_data)
technical_indicators = daily_with_indicators.iloc[-1].to_dict()
else:
technical_indicators = {}
stock_data = {
'ts_code': ts_code,
'name': stock['name'],
'industry': stock.get('industry', ''),
'financial_ratios': financial_ratios,
'technical_indicators': technical_indicators
}
if request.strategy == "comprehensive":
# 综合评估:计算三种策略的平均分
value_score = value_strategy.calculate_score(stock_data)
growth_score = growth_strategy.calculate_score(stock_data)
technical_score = technical_strategy.calculate_score(stock_data)
# 综合评分(可以调整权重)
comprehensive_score = (value_score * 0.4 + growth_score * 0.3 + technical_score * 0.3)
if comprehensive_score >= request.min_score:
stock_data['score'] = round(comprehensive_score, 2)
stock_data['value_score'] = round(value_score, 2)
stock_data['growth_score'] = round(growth_score, 2)
stock_data['technical_score'] = round(technical_score, 2)
# 综合推荐逻辑
high_scores = sum([score >= 70 for score in [value_score, growth_score, technical_score]])
if comprehensive_score >= 75 or high_scores >= 2:
stock_data['recommendation'] = 'BUY'
elif comprehensive_score >= 65:
stock_data['recommendation'] = 'HOLD'
else:
stock_data['recommendation'] = 'WATCH'
logger.debug(f"Adding stock {ts_code} with comprehensive score {comprehensive_score}")
results.append(stock_data)
else:
# 单一策略评估(保持原有逻辑)
score = strategy.calculate_score(stock_data)
if score >= request.min_score:
stock_data['score'] = round(score, 2)
stock_data['recommendation'] = 'BUY' if score >= 65 else 'HOLD'
logger.debug(f"Adding stock {ts_code} with {request.strategy} score {score}")
results.append(stock_data)
processed_count += 1
if len(results) >= request.limit:
logger.info(f"[{request_id}] Reached limit of {request.limit} results, stopping processing")
break
except Exception as e:
logger.error(f"Error processing stock {stock.get('ts_code', 'unknown')}: {e}")
continue
# 对结果进行排序
results = sorted(results, key=lambda x: x['score'], reverse=True)
logger.info(f"[{request_id}] Screening completed. Found {len(results)} qualifying stocks from {processed_count} processed")
response = {
'strategy': request.strategy,
'min_score': request.min_score,
'results': clean_data_for_json(results),
'count': len(results),
'processed_count': processed_count,
'request_id': request_id # 添加请求ID到响应中
}
cache.set(cache_key, response, 1800)
logger.info(f"[{request_id}] Returning {len(results)} results")
return response
except Exception as e:
logger.error(f"[{request_id}] Error screening stocks: {e}")
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(
"main:app",
host=Config.API_HOST,
port=Config.API_PORT,
reload=True
)

15
requirements.txt Normal file
View File

@ -0,0 +1,15 @@
fastapi==0.104.1
uvicorn==0.24.0
pandas>=2.0.0
numpy>=1.24.0
tushare==1.3.3
redis==5.0.1
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
scikit-learn>=1.3.0
requests==2.31.0
python-dotenv==1.0.0
pydantic==2.5.0
pytest==7.4.3
black==23.11.0
Flask==2.3.3

4
src/analysis/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from .financial_indicators import FinancialIndicators
from .technical_indicators import TechnicalIndicators
__all__ = ['FinancialIndicators', 'TechnicalIndicators']

View File

@ -0,0 +1,252 @@
import pandas as pd
import numpy as np
from typing import Dict, Optional
class FinancialIndicators:
@staticmethod
def safe_divide(numerator, denominator, default=0):
"""安全除法避免NaN值"""
if denominator is None or denominator == 0 or numerator is None:
return default
try:
result = float(numerator) / float(denominator)
if np.isnan(result) or np.isinf(result):
return default
return result
except (TypeError, ZeroDivisionError):
return default
@staticmethod
def safe_value(value, default=0):
"""安全值处理避免NaN和Inf"""
if value is None:
return default
try:
value = float(value)
if np.isnan(value) or np.isinf(value):
return default
return value
except (TypeError, ValueError):
return default
@staticmethod
def calculate_roe(net_income: float, shareholders_equity: float) -> float:
return FinancialIndicators.safe_divide(net_income, shareholders_equity) * 100
@staticmethod
def calculate_roa(net_income: float, total_assets: float) -> float:
return FinancialIndicators.safe_divide(net_income, total_assets) * 100
@staticmethod
def calculate_debt_ratio(total_debt: float, total_assets: float) -> float:
return FinancialIndicators.safe_divide(total_debt, total_assets) * 100
@staticmethod
def calculate_current_ratio(current_assets: float, current_liabilities: float) -> float:
return FinancialIndicators.safe_divide(current_assets, current_liabilities)
@staticmethod
def calculate_gross_margin(gross_profit: float, revenue: float) -> float:
return FinancialIndicators.safe_divide(gross_profit, revenue) * 100
@staticmethod
def calculate_net_margin(net_income: float, revenue: float) -> float:
return FinancialIndicators.safe_divide(net_income, revenue) * 100
@staticmethod
def calculate_pe_ratio(price: float, eps: float) -> float:
return FinancialIndicators.safe_divide(price, eps, default=0)
@staticmethod
def calculate_pb_ratio(price: float, book_value_per_share: float) -> float:
return FinancialIndicators.safe_divide(price, book_value_per_share, default=0)
@staticmethod
def calculate_roic(nopat: float, invested_capital: float) -> float:
"""投入资本回报率 ROIC"""
return FinancialIndicators.safe_divide(nopat, invested_capital) * 100
@staticmethod
def calculate_asset_turnover(revenue: float, total_assets: float) -> float:
"""总资产周转率"""
return FinancialIndicators.safe_divide(revenue, total_assets)
@staticmethod
def calculate_inventory_turnover(cogs: float, inventory: float) -> float:
"""存货周转率"""
return FinancialIndicators.safe_divide(cogs, inventory)
@staticmethod
def calculate_receivable_turnover(revenue: float, receivables: float) -> float:
"""应收账款周转率"""
return FinancialIndicators.safe_divide(revenue, receivables)
@staticmethod
def calculate_working_capital_ratio(working_capital: float, revenue: float) -> float:
"""营运资金比率"""
return FinancialIndicators.safe_divide(working_capital, revenue) * 100
@staticmethod
def calculate_ocf_ratio(operating_cash_flow: float, net_income: float) -> float:
"""经营现金流比率"""
return FinancialIndicators.safe_divide(operating_cash_flow, net_income) * 100
@staticmethod
def calculate_revenue_growth(current_revenue: float, previous_revenue: float) -> float:
"""营收增长率"""
if previous_revenue is None or previous_revenue == 0:
return 0
growth = FinancialIndicators.safe_divide(current_revenue - previous_revenue, previous_revenue) * 100
return FinancialIndicators.safe_value(growth)
@staticmethod
def calculate_profit_growth(current_profit: float, previous_profit: float) -> float:
"""利润增长率"""
if previous_profit is None or previous_profit == 0:
return 0
growth = FinancialIndicators.safe_divide(current_profit - previous_profit, previous_profit) * 100
return FinancialIndicators.safe_value(growth)
@staticmethod
def calculate_dupont_roe(net_margin: float, asset_turnover: float, equity_multiplier: float) -> float:
"""杜邦分析ROE = 净利率 × 总资产周转率 × 权益乘数"""
net_margin = FinancialIndicators.safe_value(net_margin)
asset_turnover = FinancialIndicators.safe_value(asset_turnover)
equity_multiplier = FinancialIndicators.safe_value(equity_multiplier)
return net_margin * asset_turnover * equity_multiplier
@staticmethod
def calculate_peg_ratio(pe_ratio: float, growth_rate: float) -> float:
if growth_rate is None or growth_rate <= 0:
return 0
return FinancialIndicators.safe_divide(pe_ratio, growth_rate, default=0)
@classmethod
def calculate_all_ratios(cls, financial_data: Dict) -> Dict:
try:
ratios = {}
# Helper function to safely get values from DataFrame
def safe_get(obj, key, default=0):
value = obj.get(key, default)
return cls.safe_value(value, default)
# 获取当前期和上一期的数据用于计算增长率
income_current = None
income_previous = None
balance_current = None
cashflow_current = None
if 'income' in financial_data and not financial_data['income'].empty:
income_current = financial_data['income'].iloc[0] if len(financial_data['income']) > 0 else None
income_previous = financial_data['income'].iloc[1] if len(financial_data['income']) > 1 else None
if 'balance' in financial_data and not financial_data['balance'].empty:
balance_current = financial_data['balance'].iloc[0]
if 'cashflow' in financial_data and not financial_data['cashflow'].empty:
cashflow_current = financial_data['cashflow'].iloc[0]
# 基础盈利能力指标
if income_current is not None:
revenue = safe_get(income_current, 'revenue')
operate_cost = safe_get(income_current, 'operate_cost')
net_income = safe_get(income_current, 'n_income')
ratios['gross_margin'] = cls.calculate_gross_margin(
revenue - operate_cost,
revenue
)
ratios['net_margin'] = cls.calculate_net_margin(
net_income,
revenue
)
# 增长率指标
if income_previous is not None:
prev_revenue = safe_get(income_previous, 'revenue')
prev_net_income = safe_get(income_previous, 'n_income')
ratios['revenue_growth'] = cls.calculate_revenue_growth(revenue, prev_revenue)
ratios['profit_growth'] = cls.calculate_profit_growth(net_income, prev_net_income)
else:
ratios['revenue_growth'] = 0
ratios['profit_growth'] = 0
# 财务健康度和效率指标
if balance_current is not None:
total_liab = safe_get(balance_current, 'total_liab')
total_assets = safe_get(balance_current, 'total_assets')
total_cur_assets = safe_get(balance_current, 'total_cur_assets')
total_cur_liab = safe_get(balance_current, 'total_cur_liab')
shareholders_equity = safe_get(balance_current, 'total_hldr_eqy_exc_min_int')
inventories = safe_get(balance_current, 'inventories')
accounts_receiv = safe_get(balance_current, 'accounts_receiv')
ratios['debt_ratio'] = cls.calculate_debt_ratio(total_liab, total_assets)
ratios['current_ratio'] = cls.calculate_current_ratio(total_cur_assets, total_cur_liab)
# 营运资金
working_capital = total_cur_assets - total_cur_liab
if income_current is not None:
revenue = safe_get(income_current, 'revenue')
ratios['working_capital_ratio'] = cls.calculate_working_capital_ratio(working_capital, revenue)
ratios['asset_turnover'] = cls.calculate_asset_turnover(revenue, total_assets)
ratios['receivable_turnover'] = cls.calculate_receivable_turnover(revenue, accounts_receiv)
# 存货周转率
operate_cost = safe_get(income_current, 'operate_cost')
ratios['inventory_turnover'] = cls.calculate_inventory_turnover(operate_cost, inventories)
else:
ratios['working_capital_ratio'] = 0
ratios['asset_turnover'] = 0
ratios['receivable_turnover'] = 0
ratios['inventory_turnover'] = 0
# ROE和ROA
if income_current is not None:
net_income = safe_get(income_current, 'n_income')
ratios['roe'] = cls.calculate_roe(net_income, shareholders_equity)
ratios['roa'] = cls.calculate_roa(net_income, total_assets)
# 权益乘数(杜邦分析组成部分)
equity_multiplier = cls.safe_divide(total_assets, shareholders_equity, default=1)
ratios['equity_multiplier'] = equity_multiplier
# 杜邦ROE分解
if 'net_margin' in ratios and 'asset_turnover' in ratios:
ratios['dupont_roe'] = cls.calculate_dupont_roe(
ratios['net_margin'] / 100, # 转换为小数
ratios['asset_turnover'],
equity_multiplier
)
else:
ratios['dupont_roe'] = 0
else:
ratios['roe'] = 0
ratios['roa'] = 0
ratios['equity_multiplier'] = 1
ratios['dupont_roe'] = 0
# 现金流指标
if cashflow_current is not None and income_current is not None:
operating_cash_flow = safe_get(cashflow_current, 'n_cashflow_act')
net_income = safe_get(income_current, 'n_income')
ratios['ocf_ratio'] = cls.calculate_ocf_ratio(operating_cash_flow, net_income)
ratios['operating_cash_flow'] = operating_cash_flow
else:
ratios['ocf_ratio'] = 0
ratios['operating_cash_flow'] = 0
# 确保所有值都是有效的
clean_ratios = {}
for key, value in ratios.items():
clean_ratios[key] = cls.safe_value(value, default=0)
return clean_ratios
except Exception as e:
print(f"Error calculating financial ratios: {e}")
return {}

View File

@ -0,0 +1,284 @@
import pandas as pd
import numpy as np
from typing import Dict, Optional
class TechnicalIndicators:
@staticmethod
def safe_value(value, default=0):
"""安全值处理避免NaN和Inf"""
if value is None:
return default
try:
value = float(value)
if np.isnan(value) or np.isinf(value):
return default
return value
except (TypeError, ValueError):
return default
@staticmethod
def calculate_sma(data: pd.Series, window: int) -> pd.Series:
return data.rolling(window=window).mean()
@staticmethod
def calculate_ema(data: pd.Series, window: int) -> pd.Series:
return data.ewm(span=window, adjust=False).mean()
@staticmethod
def calculate_rsi(data: pd.Series, window: int = 14) -> pd.Series:
delta = data.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
@staticmethod
def calculate_macd(data: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Dict[str, pd.Series]:
ema_fast = data.ewm(span=fast).mean()
ema_slow = data.ewm(span=slow).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=signal).mean()
histogram = macd_line - signal_line
return {
'macd': macd_line,
'signal': signal_line,
'histogram': histogram
}
@staticmethod
def calculate_bollinger_bands(data: pd.Series, window: int = 20, std_dev: int = 2) -> Dict[str, pd.Series]:
sma = data.rolling(window=window).mean()
std = data.rolling(window=window).std()
upper_band = sma + (std * std_dev)
lower_band = sma - (std * std_dev)
return {
'upper': upper_band,
'middle': sma,
'lower': lower_band
}
@staticmethod
def calculate_volume_sma(volume: pd.Series, window: int = 20) -> pd.Series:
return volume.rolling(window=window).mean()
@staticmethod
def calculate_price_change(data: pd.Series, periods: int = 1) -> pd.Series:
return data.pct_change(periods) * 100
@staticmethod
def calculate_stochastic_oscillator(high: pd.Series, low: pd.Series, close: pd.Series, k_window: int = 14, d_window: int = 3) -> Dict[str, pd.Series]:
"""随机震荡器 KD指标"""
lowest_low = low.rolling(window=k_window).min()
highest_high = high.rolling(window=k_window).max()
k_percent = 100 * ((close - lowest_low) / (highest_high - lowest_low))
d_percent = k_percent.rolling(window=d_window).mean()
return {
'stoch_k': k_percent,
'stoch_d': d_percent
}
@staticmethod
def calculate_williams_r(high: pd.Series, low: pd.Series, close: pd.Series, window: int = 14) -> pd.Series:
"""威廉指标 %R"""
highest_high = high.rolling(window=window).max()
lowest_low = low.rolling(window=window).min()
williams_r = -100 * ((highest_high - close) / (highest_high - lowest_low))
return williams_r
@staticmethod
def calculate_cci(high: pd.Series, low: pd.Series, close: pd.Series, window: int = 20) -> pd.Series:
"""顺势指标 CCI"""
typical_price = (high + low + close) / 3
sma_tp = typical_price.rolling(window=window).mean()
mean_deviation = typical_price.rolling(window=window).apply(lambda x: np.fabs(x - x.mean()).mean())
cci = (typical_price - sma_tp) / (0.015 * mean_deviation)
return cci
@staticmethod
def calculate_atr(high: pd.Series, low: pd.Series, close: pd.Series, window: int = 14) -> pd.Series:
"""真实波动幅度均值 ATR"""
high_low = high - low
high_close = np.abs(high - close.shift())
low_close = np.abs(low - close.shift())
true_range = np.maximum(high_low, np.maximum(high_close, low_close))
atr = true_range.rolling(window=window).mean()
return atr
@staticmethod
def calculate_adx(high: pd.Series, low: pd.Series, close: pd.Series, window: int = 14) -> Dict[str, pd.Series]:
"""趋势强度指标 ADX"""
# 计算+DI和-DI
high_diff = high.diff()
low_diff = low.diff()
plus_dm = np.where((high_diff > low_diff) & (high_diff > 0), high_diff, 0)
minus_dm = np.where((low_diff > high_diff) & (low_diff > 0), low_diff, 0)
plus_dm = pd.Series(plus_dm, index=high.index)
minus_dm = pd.Series(minus_dm, index=low.index)
# 计算真实波动幅度
tr = TechnicalIndicators.calculate_atr(high, low, close, 1)
# 计算DI
plus_di = 100 * (plus_dm.rolling(window=window).mean() / tr.rolling(window=window).mean())
minus_di = 100 * (minus_dm.rolling(window=window).mean() / tr.rolling(window=window).mean())
# 计算ADX
dx = 100 * np.abs((plus_di - minus_di) / (plus_di + minus_di))
adx = dx.rolling(window=window).mean()
return {
'adx': adx,
'plus_di': plus_di,
'minus_di': minus_di
}
@staticmethod
def calculate_momentum(data: pd.Series, window: int = 12) -> pd.Series:
"""价格动量指标"""
return data.diff(window)
@staticmethod
def calculate_rate_of_change(data: pd.Series, window: int = 12) -> pd.Series:
"""变化率指标 ROC"""
return ((data - data.shift(window)) / data.shift(window)) * 100
@staticmethod
def calculate_obv(close: pd.Series, volume: pd.Series) -> pd.Series:
"""能量潮指标 OBV"""
obv = np.zeros(len(close))
obv[0] = volume.iloc[0]
for i in range(1, len(close)):
if close.iloc[i] > close.iloc[i-1]:
obv[i] = obv[i-1] + volume.iloc[i]
elif close.iloc[i] < close.iloc[i-1]:
obv[i] = obv[i-1] - volume.iloc[i]
else:
obv[i] = obv[i-1]
return pd.Series(obv, index=close.index)
@staticmethod
def calculate_volume_ratio(volume: pd.Series, window: int = 20) -> pd.Series:
"""量比指标"""
volume_ma = volume.rolling(window=window).mean()
return volume / volume_ma
@staticmethod
def calculate_price_volume_trend(close: pd.Series, volume: pd.Series) -> pd.Series:
"""价量趋势指标 PVT"""
price_change_pct = close.pct_change()
pvt = (price_change_pct * volume).cumsum()
return pvt
@staticmethod
def calculate_volatility(data: pd.Series, window: int = 20) -> pd.Series:
returns = data.pct_change()
return returns.rolling(window=window).std() * np.sqrt(252) * 100
@classmethod
def calculate_all_indicators(cls, df: pd.DataFrame) -> pd.DataFrame:
result = df.copy()
close_price = df['close']
high_price = df.get('high', close_price)
low_price = df.get('low', close_price)
volume = df.get('vol', pd.Series())
try:
# 移动平均线系统
result['sma_20'] = cls.calculate_sma(close_price, 20).fillna(0)
result['sma_50'] = cls.calculate_sma(close_price, 50).fillna(0)
result['sma_200'] = cls.calculate_sma(close_price, 200).fillna(0)
result['ema_12'] = cls.calculate_ema(close_price, 12).fillna(0)
result['ema_26'] = cls.calculate_ema(close_price, 26).fillna(0)
# 动量和震荡指标
result['rsi'] = cls.calculate_rsi(close_price).fillna(50)
result['momentum'] = cls.calculate_momentum(close_price).fillna(0)
result['roc'] = cls.calculate_rate_of_change(close_price).fillna(0)
# MACD系统
macd_data = cls.calculate_macd(close_price)
result['macd'] = macd_data['macd'].fillna(0)
result['macd_signal'] = macd_data['signal'].fillna(0)
result['macd_histogram'] = macd_data['histogram'].fillna(0)
# KD随机指标
if 'high' in df.columns and 'low' in df.columns:
stoch_data = cls.calculate_stochastic_oscillator(high_price, low_price, close_price)
result['stoch_k'] = stoch_data['stoch_k'].fillna(50)
result['stoch_d'] = stoch_data['stoch_d'].fillna(50)
# 威廉指标
result['williams_r'] = cls.calculate_williams_r(high_price, low_price, close_price).fillna(-50)
# CCI顺势指标
result['cci'] = cls.calculate_cci(high_price, low_price, close_price).fillna(0)
# ATR真实波动幅度
result['atr'] = cls.calculate_atr(high_price, low_price, close_price).fillna(0)
# ADX趋势强度
adx_data = cls.calculate_adx(high_price, low_price, close_price)
result['adx'] = adx_data['adx'].fillna(25)
result['plus_di'] = adx_data['plus_di'].fillna(20)
result['minus_di'] = adx_data['minus_di'].fillna(20)
else:
# 填充默认值
result['stoch_k'] = 50
result['stoch_d'] = 50
result['williams_r'] = -50
result['cci'] = 0
result['atr'] = 0
result['adx'] = 25
result['plus_di'] = 20
result['minus_di'] = 20
# 布林带系统
bollinger = cls.calculate_bollinger_bands(close_price)
result['bb_upper'] = bollinger['upper'].fillna(close_price)
result['bb_middle'] = bollinger['middle'].fillna(close_price)
result['bb_lower'] = bollinger['lower'].fillna(close_price)
# 布林带宽度(量化波动性)
bb_width = (result['bb_upper'] - result['bb_lower']) / result['bb_middle'] * 100
result['bb_width'] = bb_width.fillna(0)
# 价格变化和波动率
result['price_change'] = cls.calculate_price_change(close_price).fillna(0)
result['volatility'] = cls.calculate_volatility(close_price).fillna(20)
# 成交量指标
if not volume.empty:
result['volume_sma'] = cls.calculate_volume_sma(volume).fillna(volume.mean())
result['volume_ratio'] = cls.calculate_volume_ratio(volume).fillna(1)
result['obv'] = cls.calculate_obv(close_price, volume).fillna(0)
result['pvt'] = cls.calculate_price_volume_trend(close_price, volume).fillna(0)
result['current_volume'] = volume
else:
result['volume_sma'] = 0
result['volume_ratio'] = 1
result['obv'] = 0
result['pvt'] = 0
result['current_volume'] = 0
# 添加当前价格到技术指标中(用于策略计算)
result['current_price'] = close_price
# 确保所有数值都是安全的
for col in result.columns:
if result[col].dtype in ['float64', 'float32']:
result[col] = result[col].replace([np.inf, -np.inf], 0).fillna(0)
return result
except Exception as e:
print(f"Error calculating technical indicators: {e}")
return result

View File

@ -0,0 +1,461 @@
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Tuple
from .technical_indicators import TechnicalIndicators
class TradingSignalsAnalyzer:
"""技术分析交易信号分析器 - 专门为A股市场设计"""
def __init__(self):
self.signals = {}
@staticmethod
def safe_value(value, default=0):
"""安全值处理"""
if value is None or pd.isna(value):
return default
try:
value = float(value)
if np.isnan(value) or np.isinf(value):
return default
return value
except (TypeError, ValueError):
return default
def analyze_entry_signals(self, technical_data: Dict) -> Dict:
"""分析买入信号"""
signals = {
'overall_signal': 'NEUTRAL',
'signal_strength': 0, # 0-100
'entry_reasons': [],
'warning_flags': [],
'confidence_level': 'LOW' # LOW/MEDIUM/HIGH
}
signal_count = 0
total_signals = 0
# 1. 趋势突破信号
trend_signal = self._analyze_trend_breakthrough(technical_data)
if trend_signal['signal'] == 'BUY':
signal_count += trend_signal['weight']
signals['entry_reasons'].append(trend_signal['reason'])
total_signals += trend_signal['weight']
# 2. 动量确认信号
momentum_signal = self._analyze_momentum_confirmation(technical_data)
if momentum_signal['signal'] == 'BUY':
signal_count += momentum_signal['weight']
signals['entry_reasons'].append(momentum_signal['reason'])
total_signals += momentum_signal['weight']
# 3. 成交量确认
volume_signal = self._analyze_volume_confirmation(technical_data)
if volume_signal['signal'] == 'BUY':
signal_count += volume_signal['weight']
signals['entry_reasons'].append(volume_signal['reason'])
elif volume_signal['signal'] == 'WARNING':
signals['warning_flags'].append(volume_signal['reason'])
total_signals += volume_signal['weight']
# 4. 支撑阻力位分析
support_signal = self._analyze_support_levels(technical_data)
if support_signal['signal'] == 'BUY':
signal_count += support_signal['weight']
signals['entry_reasons'].append(support_signal['reason'])
total_signals += support_signal['weight']
# 5. 超卖反弹机会
oversold_signal = self._analyze_oversold_opportunity(technical_data)
if oversold_signal['signal'] == 'BUY':
signal_count += oversold_signal['weight']
signals['entry_reasons'].append(oversold_signal['reason'])
total_signals += oversold_signal['weight']
# 计算总体信号强度
if total_signals > 0:
signals['signal_strength'] = int((signal_count / total_signals) * 100)
# 确定总体信号
if signals['signal_strength'] >= 75:
signals['overall_signal'] = 'STRONG_BUY'
signals['confidence_level'] = 'HIGH'
elif signals['signal_strength'] >= 60:
signals['overall_signal'] = 'BUY'
signals['confidence_level'] = 'MEDIUM'
elif signals['signal_strength'] >= 40:
signals['overall_signal'] = 'WEAK_BUY'
signals['confidence_level'] = 'LOW'
else:
signals['overall_signal'] = 'NEUTRAL'
return signals
def analyze_exit_signals(self, technical_data: Dict) -> Dict:
"""分析卖出信号"""
signals = {
'overall_signal': 'HOLD',
'signal_strength': 0,
'exit_reasons': [],
'risk_flags': [],
'confidence_level': 'LOW'
}
signal_count = 0
total_signals = 0
# 1. 趋势破坏信号
trend_break = self._analyze_trend_breakdown(technical_data)
if trend_break['signal'] == 'SELL':
signal_count += trend_break['weight']
signals['exit_reasons'].append(trend_break['reason'])
total_signals += trend_break['weight']
# 2. 动量转弱信号
momentum_weak = self._analyze_momentum_weakness(technical_data)
if momentum_weak['signal'] == 'SELL':
signal_count += momentum_weak['weight']
signals['exit_reasons'].append(momentum_weak['reason'])
total_signals += momentum_weak['weight']
# 3. 超买风险
overbought_risk = self._analyze_overbought_risk(technical_data)
if overbought_risk['signal'] == 'SELL':
signal_count += overbought_risk['weight']
signals['exit_reasons'].append(overbought_risk['reason'])
total_signals += overbought_risk['weight']
# 4. 成交量背离
volume_divergence = self._analyze_volume_divergence(technical_data)
if volume_divergence['signal'] == 'SELL':
signal_count += volume_divergence['weight']
signals['risk_flags'].append(volume_divergence['reason'])
total_signals += volume_divergence['weight']
# 计算信号强度
if total_signals > 0:
signals['signal_strength'] = int((signal_count / total_signals) * 100)
# 确定卖出信号
if signals['signal_strength'] >= 75:
signals['overall_signal'] = 'STRONG_SELL'
signals['confidence_level'] = 'HIGH'
elif signals['signal_strength'] >= 60:
signals['overall_signal'] = 'SELL'
signals['confidence_level'] = 'MEDIUM'
elif signals['signal_strength'] >= 40:
signals['overall_signal'] = 'WEAK_SELL'
signals['confidence_level'] = 'LOW'
return signals
def calculate_stop_loss_take_profit(self, technical_data: Dict, entry_price: float = None) -> Dict:
"""计算止盈止损位"""
current_price = self.safe_value(technical_data.get('current_price', 0))
if entry_price is None:
entry_price = current_price
atr = self.safe_value(technical_data.get('atr', 0))
bb_upper = self.safe_value(technical_data.get('bb_upper', 0))
bb_lower = self.safe_value(technical_data.get('bb_lower', 0))
sma_20 = self.safe_value(technical_data.get('sma_20', 0))
volatility = self.safe_value(technical_data.get('volatility', 20))
# A股市场特点10%涨跌停板限制
max_daily_change = 0.10
# 1. ATR动态止损法
atr_stop_loss = entry_price - (atr * 2) if atr > 0 else entry_price * 0.92
atr_take_profit = entry_price + (atr * 3) if atr > 0 else entry_price * 1.15
# 2. 技术支撑止损法
if sma_20 > 0 and sma_20 < entry_price:
support_stop_loss = sma_20 * 0.98 # SMA20下方2%
else:
support_stop_loss = entry_price * 0.95
# 3. 布林带止盈止损法
if bb_lower > 0 and bb_upper > 0:
bollinger_stop_loss = bb_lower * 0.99
bollinger_take_profit = bb_upper * 0.99
else:
bollinger_stop_loss = entry_price * 0.93
bollinger_take_profit = entry_price * 1.12
# 4. 波动率自适应方法
volatility_multiplier = max(1.5, min(3.0, volatility / 20)) # 1.5-3倍波动率
volatility_stop_loss = entry_price * (1 - 0.05 * volatility_multiplier)
volatility_take_profit = entry_price * (1 + 0.08 * volatility_multiplier)
# 综合计算推荐的止盈止损位
stop_loss_candidates = [atr_stop_loss, support_stop_loss, bollinger_stop_loss, volatility_stop_loss]
take_profit_candidates = [atr_take_profit, bollinger_take_profit, volatility_take_profit]
# 选择相对保守的止损位(较高的那个)
recommended_stop_loss = max([sl for sl in stop_loss_candidates if sl > entry_price * 0.85])
# 选择合理的止盈位(避免过于激进)
recommended_take_profit = min([tp for tp in take_profit_candidates if tp < entry_price * 1.25])
# 确保符合A股涨跌停限制
max_price = current_price * (1 + max_daily_change)
min_price = current_price * (1 - max_daily_change)
return {
'stop_loss': {
'recommended': round(max(recommended_stop_loss, min_price), 2),
'atr_based': round(max(atr_stop_loss, min_price), 2),
'support_based': round(max(support_stop_loss, min_price), 2),
'risk_ratio': round(abs(entry_price - recommended_stop_loss) / entry_price * 100, 2)
},
'take_profit': {
'recommended': round(min(recommended_take_profit, max_price), 2),
'conservative': round(min(entry_price * 1.08, max_price), 2),
'aggressive': round(min(entry_price * 1.18, max_price), 2),
'reward_ratio': round((recommended_take_profit - entry_price) / entry_price * 100, 2)
},
'risk_reward_ratio': round((recommended_take_profit - entry_price) / (entry_price - recommended_stop_loss), 2) if entry_price != recommended_stop_loss else 0,
'position_sizing_suggestion': self._calculate_position_size(entry_price, recommended_stop_loss, volatility)
}
def _analyze_trend_breakthrough(self, data: Dict) -> Dict:
"""趋势突破分析"""
current_price = self.safe_value(data.get('current_price'))
sma_20 = self.safe_value(data.get('sma_20'))
sma_50 = self.safe_value(data.get('sma_50'))
volume_ratio = self.safe_value(data.get('volume_ratio', 1))
if current_price > sma_20 > sma_50 and volume_ratio > 1.2:
return {
'signal': 'BUY',
'weight': 3,
'reason': '价格突破20日均线趋势向上成交量配合'
}
elif current_price > sma_20 and volume_ratio > 1.5:
return {
'signal': 'BUY',
'weight': 2,
'reason': '价格站上20日均线放量突破'
}
return {'signal': 'NEUTRAL', 'weight': 3, 'reason': ''}
def _analyze_momentum_confirmation(self, data: Dict) -> Dict:
"""动量确认分析"""
rsi = self.safe_value(data.get('rsi', 50))
macd = self.safe_value(data.get('macd', 0))
macd_signal = self.safe_value(data.get('macd_signal', 0))
stoch_k = self.safe_value(data.get('stoch_k', 50))
positive_signals = 0
if 30 <= rsi <= 70: # RSI健康区间
positive_signals += 1
if macd > macd_signal: # MACD金叉
positive_signals += 1
if 20 <= stoch_k <= 80: # KD指标适中
positive_signals += 1
if positive_signals >= 2:
return {
'signal': 'BUY',
'weight': 2,
'reason': f'多个动量指标确认买入信号({positive_signals}/3)'
}
return {'signal': 'NEUTRAL', 'weight': 2, 'reason': ''}
def _analyze_volume_confirmation(self, data: Dict) -> Dict:
"""成交量确认分析"""
volume_ratio = self.safe_value(data.get('volume_ratio', 1))
obv = self.safe_value(data.get('obv', 0))
if volume_ratio >= 1.8:
return {
'signal': 'BUY',
'weight': 2,
'reason': f'显著放量({volume_ratio:.1f}倍),资金流入明显'
}
elif volume_ratio >= 1.3:
return {
'signal': 'BUY',
'weight': 1,
'reason': f'温和放量({volume_ratio:.1f}倍),有资金关注'
}
elif volume_ratio < 0.7:
return {
'signal': 'WARNING',
'weight': 1,
'reason': '成交量萎缩,市场关注度低'
}
return {'signal': 'NEUTRAL', 'weight': 2, 'reason': ''}
def _analyze_support_levels(self, data: Dict) -> Dict:
"""支撑位分析"""
current_price = self.safe_value(data.get('current_price'))
bb_lower = self.safe_value(data.get('bb_lower'))
sma_20 = self.safe_value(data.get('sma_20'))
if current_price > 0 and bb_lower > 0:
distance_to_support = (current_price - bb_lower) / current_price
if 0.02 <= distance_to_support <= 0.05: # 接近支撑位但未跌破
return {
'signal': 'BUY',
'weight': 2,
'reason': '接近布林带下轨支撑位,反弹概率高'
}
if current_price > sma_20 and sma_20 > 0:
return {
'signal': 'BUY',
'weight': 1,
'reason': '价格站稳20日均线支撑'
}
return {'signal': 'NEUTRAL', 'weight': 2, 'reason': ''}
def _analyze_oversold_opportunity(self, data: Dict) -> Dict:
"""超卖反弹机会分析"""
rsi = self.safe_value(data.get('rsi', 50))
williams_r = self.safe_value(data.get('williams_r', -50))
stoch_k = self.safe_value(data.get('stoch_k', 50))
oversold_count = 0
if rsi < 30:
oversold_count += 1
if williams_r < -80:
oversold_count += 1
if stoch_k < 20:
oversold_count += 1
if oversold_count >= 2:
return {
'signal': 'BUY',
'weight': 2,
'reason': f'多指标显示超卖({oversold_count}/3),反弹机会大'
}
return {'signal': 'NEUTRAL', 'weight': 2, 'reason': ''}
def _analyze_trend_breakdown(self, data: Dict) -> Dict:
"""趋势破坏分析"""
current_price = self.safe_value(data.get('current_price'))
sma_20 = self.safe_value(data.get('sma_20'))
sma_50 = self.safe_value(data.get('sma_50'))
volume_ratio = self.safe_value(data.get('volume_ratio', 1))
if current_price < sma_20 < sma_50 and volume_ratio > 1.2:
return {
'signal': 'SELL',
'weight': 3,
'reason': '跌破20日均线趋势转弱放量下跌'
}
elif current_price < sma_20:
return {
'signal': 'SELL',
'weight': 2,
'reason': '跌破20日均线支撑'
}
return {'signal': 'NEUTRAL', 'weight': 3, 'reason': ''}
def _analyze_momentum_weakness(self, data: Dict) -> Dict:
"""动量转弱分析"""
rsi = self.safe_value(data.get('rsi', 50))
macd = self.safe_value(data.get('macd', 0))
macd_signal = self.safe_value(data.get('macd_signal', 0))
adx = self.safe_value(data.get('adx', 25))
weakness_signals = 0
if rsi > 70: # RSI超买
weakness_signals += 1
if macd < macd_signal: # MACD死叉
weakness_signals += 1
if adx < 20: # 趋势力度减弱
weakness_signals += 1
if weakness_signals >= 2:
return {
'signal': 'SELL',
'weight': 2,
'reason': f'动量指标显示趋势转弱({weakness_signals}/3)'
}
return {'signal': 'NEUTRAL', 'weight': 2, 'reason': ''}
def _analyze_overbought_risk(self, data: Dict) -> Dict:
"""超买风险分析"""
rsi = self.safe_value(data.get('rsi', 50))
stoch_k = self.safe_value(data.get('stoch_k', 50))
bb_position = self._calculate_bb_position(data)
overbought_signals = 0
if rsi > 80:
overbought_signals += 1
if stoch_k > 80:
overbought_signals += 1
if bb_position > 0.9:
overbought_signals += 1
if overbought_signals >= 2:
return {
'signal': 'SELL',
'weight': 2,
'reason': f'多指标显示超买({overbought_signals}/3),回调风险高'
}
return {'signal': 'NEUTRAL', 'weight': 2, 'reason': ''}
def _analyze_volume_divergence(self, data: Dict) -> Dict:
"""成交量背离分析"""
volume_ratio = self.safe_value(data.get('volume_ratio', 1))
obv = self.safe_value(data.get('obv', 0))
if volume_ratio < 0.5:
return {
'signal': 'SELL',
'weight': 1,
'reason': '成交量严重萎缩,市场参与度低'
}
return {'signal': 'NEUTRAL', 'weight': 1, 'reason': ''}
def _calculate_bb_position(self, data: Dict) -> float:
"""计算布林带位置"""
current_price = self.safe_value(data.get('current_price'))
bb_upper = self.safe_value(data.get('bb_upper'))
bb_lower = self.safe_value(data.get('bb_lower'))
if bb_upper > bb_lower and current_price > 0:
return (current_price - bb_lower) / (bb_upper - bb_lower)
return 0.5
def _calculate_position_size(self, entry_price: float, stop_loss: float, volatility: float) -> Dict:
"""计算建议仓位"""
risk_per_trade = 0.02 # 单笔交易风险2%
max_position = 0.20 # 最大单股仓位20%
if entry_price <= stop_loss:
return {'suggested_position': 0, 'reason': '止损位设置不当'}
risk_amount = abs(entry_price - stop_loss) / entry_price
# 基于风险调整仓位
if risk_amount > 0:
position_size = min(risk_per_trade / risk_amount, max_position)
else:
position_size = 0.05 # 默认5%
# 根据波动率调整
if volatility > 40: # 高波动
position_size *= 0.7
elif volatility > 60: # 极高波动
position_size *= 0.5
return {
'suggested_position': round(position_size * 100, 1), # 转换为百分比
'risk_amount': round(risk_amount * 100, 2),
'reason': f'基于{risk_amount*100:.1f}%风险和{volatility:.1f}%波动率计算'
}

5
src/data/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from .tushare_client import TushareClient
from .database import Database
from .cache import RedisCache
__all__ = ['TushareClient', 'Database', 'RedisCache']

64
src/data/cache.py Normal file
View File

@ -0,0 +1,64 @@
import redis
import json
import logging
from typing import Any, Optional
from config.config import Config
logger = logging.getLogger(__name__)
class RedisCache:
def __init__(self):
try:
self.redis_client = redis.Redis(
host=Config.REDIS_HOST,
port=Config.REDIS_PORT,
db=Config.REDIS_DB,
decode_responses=True
)
self.redis_client.ping()
except redis.ConnectionError:
logger.warning("Redis connection failed, using dummy cache")
self.redis_client = None
def get(self, key: str) -> Optional[Any]:
if not self.redis_client:
return None
try:
data = self.redis_client.get(key)
return json.loads(data) if data else None
except Exception as e:
logger.error(f"Redis get error: {e}")
return None
def set(self, key: str, value: Any, expire: int = None) -> bool:
if not self.redis_client:
return False
try:
expire = expire or Config.CACHE_EXPIRE
data = json.dumps(value, default=str)
return self.redis_client.setex(key, expire, data)
except Exception as e:
logger.error(f"Redis set error: {e}")
return False
def delete(self, key: str) -> bool:
if not self.redis_client:
return False
try:
return bool(self.redis_client.delete(key))
except Exception as e:
logger.error(f"Redis delete error: {e}")
return False
def exists(self, key: str) -> bool:
if not self.redis_client:
return False
try:
return bool(self.redis_client.exists(key))
except Exception as e:
logger.error(f"Redis exists error: {e}")
return False

86
src/data/database.py Normal file
View File

@ -0,0 +1,86 @@
from sqlalchemy import create_engine, Column, String, Float, DateTime, Integer, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import pandas as pd
from config.config import Config
import logging
logger = logging.getLogger(__name__)
Base = declarative_base()
class StockBasic(Base):
__tablename__ = 'stock_basic'
ts_code = Column(String(20), primary_key=True)
symbol = Column(String(10))
name = Column(String(50))
area = Column(String(20))
industry = Column(String(50))
market = Column(String(10))
list_date = Column(String(10))
class StockDaily(Base):
__tablename__ = 'stock_daily'
id = Column(Integer, primary_key=True, autoincrement=True)
ts_code = Column(String(20))
trade_date = Column(String(10))
open = Column(Float)
high = Column(Float)
low = Column(Float)
close = Column(Float)
pre_close = Column(Float)
change = Column(Float)
pct_chg = Column(Float)
vol = Column(Float)
amount = Column(Float)
class Database:
def __init__(self, database_url: str = None):
self.database_url = database_url or Config.DATABASE_URL
self.engine = create_engine(self.database_url)
Base.metadata.create_all(self.engine)
Session = sessionmaker(bind=self.engine)
self.session = Session()
def save_stock_basic(self, df: pd.DataFrame) -> bool:
try:
df.to_sql('stock_basic', self.engine, if_exists='replace', index=False)
return True
except Exception as e:
logger.error(f"Failed to save stock basic data: {e}")
return False
def save_stock_daily(self, df: pd.DataFrame) -> bool:
try:
df.to_sql('stock_daily', self.engine, if_exists='append', index=False)
return True
except Exception as e:
logger.error(f"Failed to save stock daily data: {e}")
return False
def get_stock_list(self) -> pd.DataFrame:
try:
return pd.read_sql_table('stock_basic', self.engine)
except Exception as e:
logger.error(f"Failed to get stock list: {e}")
return pd.DataFrame()
def get_stock_daily(self, ts_code: str, start_date: str = None, end_date: str = None) -> pd.DataFrame:
try:
query = f"SELECT * FROM stock_daily WHERE ts_code = '{ts_code}'"
if start_date:
query += f" AND trade_date >= '{start_date}'"
if end_date:
query += f" AND trade_date <= '{end_date}'"
query += " ORDER BY trade_date DESC"
return pd.read_sql_query(query, self.engine)
except Exception as e:
logger.error(f"Failed to get stock daily data: {e}")
return pd.DataFrame()
def close(self):
self.session.close()

142
src/data/tushare_client.py Normal file
View File

@ -0,0 +1,142 @@
import tushare as ts
import pandas as pd
import logging
from typing import Optional, Dict, List
from datetime import datetime, timedelta
from config.config import Config
logger = logging.getLogger(__name__)
class TushareClient:
def __init__(self, token: str = None):
self.token = token or Config.TUSHARE_TOKEN
if not self.token:
raise ValueError("Tushare token is required")
ts.set_token(self.token)
self.pro = ts.pro_api()
def get_stock_list(self, exchange: str = None) -> pd.DataFrame:
try:
return self.pro.stock_basic(exchange=exchange, list_status='L')
except Exception as e:
logger.error(f"Failed to get stock list: {e}")
return pd.DataFrame()
def get_stock_daily(self, ts_code: str, start_date: str = None,
end_date: str = None) -> pd.DataFrame:
try:
if not start_date:
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
if not end_date:
end_date = datetime.now().strftime('%Y%m%d')
return self.pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
except Exception as e:
logger.error(f"Failed to get daily data for {ts_code}: {e}")
return pd.DataFrame()
def get_financial_data(self, ts_code: str, period: str = None) -> Dict[str, pd.DataFrame]:
try:
data = {}
income = self.pro.income(ts_code=ts_code, period=period)
data['income'] = income
balance = self.pro.balancesheet(ts_code=ts_code, period=period)
data['balance'] = balance
cashflow = self.pro.cashflow(ts_code=ts_code, period=period)
data['cashflow'] = cashflow
return data
except Exception as e:
logger.error(f"Failed to get financial data for {ts_code}: {e}")
return {}
def get_industry_classify(self, ts_code: str = None) -> pd.DataFrame:
try:
return self.pro.industry_classify(ts_code=ts_code)
except Exception as e:
logger.error(f"Failed to get industry classify: {e}")
return pd.DataFrame()
def get_hs300_stocks(self) -> pd.DataFrame:
"""获取沪深300成分股列表"""
try:
# 获取沪深300成分股
hs300_stocks = self.pro.index_weight(index_code='399300.SZ')
if hs300_stocks.empty:
logger.warning("No HS300 stocks found, fallback to CSI300")
# 备选使用中证300
hs300_stocks = self.pro.index_weight(index_code='000300.SH')
if hs300_stocks.empty:
logger.error("No HS300 component stocks found")
return pd.DataFrame()
# 获取这些股票的基本信息
ts_codes = hs300_stocks['con_code'].tolist()
stock_basic_list = []
# 批量获取股票基本信息
for i in range(0, len(ts_codes), 50): # 每次获取50只股票
batch_codes = ts_codes[i:i+50]
try:
batch_info = self.pro.stock_basic(ts_code=','.join(batch_codes))
if not batch_info.empty:
stock_basic_list.append(batch_info)
except Exception as e:
logger.warning(f"Failed to get batch stock info: {e}")
continue
if stock_basic_list:
result = pd.concat(stock_basic_list, ignore_index=True)
logger.info(f"Successfully retrieved {len(result)} HS300 component stocks")
return result
else:
logger.error("Failed to get any HS300 stock basic info")
return pd.DataFrame()
except Exception as e:
logger.error(f"Failed to get HS300 stocks: {e}")
return pd.DataFrame()
def get_stock_basic(self, ts_code: str) -> Optional[Dict]:
"""获取股票基本信息"""
try:
# 获取股票基本信息
basic_info = self.pro.stock_basic(ts_code=ts_code)
if basic_info.empty:
return None
# 获取最新的日行情数据(当前价格和总市值)
latest_daily = self.pro.daily_basic(ts_code=ts_code, limit=1)
# 组合基本信息
result = {
'ts_code': ts_code,
'name': basic_info.iloc[0]['name'] if not basic_info.empty else '',
'industry': basic_info.iloc[0]['industry'] if not basic_info.empty else '',
'list_date': basic_info.iloc[0]['list_date'] if not basic_info.empty else '',
'current_price': latest_daily.iloc[0]['close'] if not latest_daily.empty else 0,
'market_cap': latest_daily.iloc[0]['total_mv'] if not latest_daily.empty and 'total_mv' in latest_daily.columns else 0,
}
return result
except Exception as e:
logger.error(f"Failed to get stock basic info for {ts_code}: {e}")
return None
def get_trade_cal(self, start_date: str = None, end_date: str = None) -> pd.DataFrame:
try:
if not start_date:
start_date = datetime.now().strftime('%Y%m%d')
if not end_date:
end_date = (datetime.now() + timedelta(days=30)).strftime('%Y%m%d')
return self.pro.trade_cal(start_date=start_date, end_date=end_date)
except Exception as e:
logger.error(f"Failed to get trade calendar: {e}")
return pd.DataFrame()

View File

@ -0,0 +1,6 @@
from .base_strategy import BaseStrategy
from .value_strategy import ValueStrategy
from .growth_strategy import GrowthStrategy
from .technical_strategy import TechnicalStrategy
__all__ = ['BaseStrategy', 'ValueStrategy', 'GrowthStrategy', 'TechnicalStrategy']

View File

@ -0,0 +1,45 @@
from abc import ABC, abstractmethod
import pandas as pd
from typing import Dict, List
class BaseStrategy(ABC):
def __init__(self, name: str, description: str):
self.name = name
self.description = description
self.weights = {}
@abstractmethod
def calculate_score(self, stock_data: Dict) -> float:
pass
@abstractmethod
def get_criteria(self) -> Dict:
pass
def filter_stocks(self, stocks_data: List[Dict], min_score: float = 60) -> List[Dict]:
filtered_stocks = []
for stock in stocks_data:
try:
score = self.calculate_score(stock)
if score >= min_score:
stock['strategy_score'] = score
stock['strategy_name'] = self.name
filtered_stocks.append(stock)
except Exception as e:
print(f"Error calculating score for {stock.get('ts_code', 'unknown')}: {e}")
continue
return sorted(filtered_stocks, key=lambda x: x['strategy_score'], reverse=True)
def rank_stocks(self, stocks_data: List[Dict]) -> List[Dict]:
for stock in stocks_data:
try:
stock['strategy_score'] = self.calculate_score(stock)
stock['strategy_name'] = self.name
except Exception as e:
print(f"Error calculating score for {stock.get('ts_code', 'unknown')}: {e}")
stock['strategy_score'] = 0
return sorted(stocks_data, key=lambda x: x['strategy_score'], reverse=True)

View File

@ -0,0 +1,176 @@
from .base_strategy import BaseStrategy
from typing import Dict
class GrowthStrategy(BaseStrategy):
def __init__(self):
super().__init__(
name="成长投资策略",
description="基于高增长率、强盈利能力、良好发展前景的成长股选股策略"
)
self.weights = {
'revenue_growth_score': 0.25,
'profit_growth_score': 0.20,
'roe_score': 0.15,
'margin_trend_score': 0.12,
'innovation_score': 0.08,
'asset_turnover_score': 0.08, # 新增资产效率
'working_capital_score': 0.07, # 新增营运资金管理
'dupont_roe_score': 0.05 # 新增杜邦分析
}
def calculate_score(self, stock_data: Dict) -> float:
try:
financial_ratios = stock_data.get('financial_ratios', {})
revenue_growth = financial_ratios.get('revenue_growth', 0)
profit_growth = financial_ratios.get('profit_growth', 0)
roe = financial_ratios.get('roe', 0)
margin_trend = financial_ratios.get('margin_trend', 0)
rd_expense_ratio = financial_ratios.get('rd_expense_ratio', 0)
asset_turnover = financial_ratios.get('asset_turnover', 0) # 新增
working_capital_ratio = financial_ratios.get('working_capital_ratio', 0) # 新增
dupont_roe = financial_ratios.get('dupont_roe', 0) # 新增
revenue_growth_score = self._score_revenue_growth(revenue_growth)
profit_growth_score = self._score_profit_growth(profit_growth)
roe_score = self._score_roe(roe)
margin_trend_score = self._score_margin_trend(margin_trend)
innovation_score = self._score_rd_expense(rd_expense_ratio)
asset_turnover_score = self._score_asset_turnover(asset_turnover) # 新增
working_capital_score = self._score_working_capital(working_capital_ratio) # 新增
dupont_roe_score = self._score_dupont_roe(dupont_roe) # 新增
total_score = (
revenue_growth_score * self.weights['revenue_growth_score'] +
profit_growth_score * self.weights['profit_growth_score'] +
roe_score * self.weights['roe_score'] +
margin_trend_score * self.weights['margin_trend_score'] +
innovation_score * self.weights['innovation_score'] +
asset_turnover_score * self.weights['asset_turnover_score'] +
working_capital_score * self.weights['working_capital_score'] +
dupont_roe_score * self.weights['dupont_roe_score']
)
return total_score
except Exception as e:
print(f"Error in GrowthStrategy.calculate_score: {e}")
return 0
def _score_revenue_growth(self, growth_rate: float) -> float:
"""营收增长率评分 - 适应A股实际情况"""
if growth_rate >= 25: # 超高增长
return 100
elif growth_rate >= 15: # 高增长
return 80
elif growth_rate >= 8: # 中等增长
return 60
elif growth_rate >= 3: # 低增长
return 40
elif growth_rate >= 0: # 正增长
return 30
else: # 负增长
return 10
def _score_profit_growth(self, growth_rate: float) -> float:
"""利润增长率评分 - 更现实的标准"""
if growth_rate >= 40: # 超高增长
return 100
elif growth_rate >= 20: # 高增长
return 80
elif growth_rate >= 10: # 中等增长
return 60
elif growth_rate >= 3: # 低增长
return 40
elif growth_rate >= 0: # 正增长
return 30
else: # 负增长
return 10
def _score_roe(self, roe: float) -> float:
"""ROE评分 - 成长股标准"""
if roe >= 20: # 优秀盈利能力
return 100
elif roe >= 15: # 良好盈利能力
return 80
elif roe >= 10: # 中等盈利能力
return 60
elif roe >= 5: # 较低盈利能力
return 40
else: # 盈利能力差
return 20
def _score_margin_trend(self, margin_trend: float) -> float:
"""利润率趋势评分 - 适应市场波动"""
if margin_trend >= 1.5: # 显著改善
return 100
elif margin_trend >= 0.5: # 明显改善
return 80
elif margin_trend >= 0: # 轻微改善
return 60
elif margin_trend >= -0.5: # 轻微恶化
return 40
else: # 明显恶化
return 20
def _score_rd_expense(self, rd_ratio: float) -> float:
"""研发投入评分 - 符合A股研发水平"""
if rd_ratio >= 8: # 高研发投入
return 100
elif rd_ratio >= 4: # 较高研发投入
return 80
elif rd_ratio >= 2: # 中等研发投入
return 60
elif rd_ratio >= 0.5: # 较低研发投入
return 40
else: # 无或极低研发投入
return 20
def _score_asset_turnover(self, asset_turnover: float) -> float:
"""资产周转率评分 - 成长股注重效率"""
if asset_turnover >= 1.5: # 高效率
return 100
elif asset_turnover >= 1.0: # 较高效率
return 80
elif asset_turnover >= 0.7: # 中等效率
return 60
elif asset_turnover >= 0.5: # 较低效率
return 40
else: # 低效率
return 20
def _score_working_capital(self, wc_ratio: float) -> float:
"""营运资金管理评分 - 成长股资金使用效率"""
if 5 <= wc_ratio <= 15: # 适度的营运资金比例
return 100
elif 0 <= wc_ratio <= 25: # 较合理的营运资金
return 80
elif -5 <= wc_ratio <= 30: # 可接受的范围
return 60
elif wc_ratio <= 40: # 营运资金过多,效率不高
return 40
else: # 营运资金管理不佳
return 20
def _score_dupont_roe(self, dupont_roe: float) -> float:
"""杜邦分析ROE评分 - 综合盈利能力分解"""
if dupont_roe >= 20: # 优秀的综合盈利能力
return 100
elif dupont_roe >= 15: # 良好的综合盈利能力
return 80
elif dupont_roe >= 10: # 中等的综合盈利能力
return 60
elif dupont_roe >= 5: # 较低的综合盈利能力
return 40
else: # 差的综合盈利能力
return 20
def get_criteria(self) -> Dict:
return {
'min_revenue_growth': 5,
'min_profit_growth': 5,
'min_roe': 10,
'min_margin_trend': -1,
'description': '从沪深300成分股中寻找高增长、高盈利、有创新能力的成长股'
}

View File

@ -0,0 +1,519 @@
from .base_strategy import BaseStrategy
from typing import Dict
from ..analysis.trading_signals import TradingSignalsAnalyzer
class TechnicalStrategy(BaseStrategy):
def __init__(self):
super().__init__(
name="技术分析策略",
description="基于技术指标、价格趋势、交易量的技术分析选股策略"
)
self.weights = {
'trend_score': 0.25,
'momentum_score': 0.20,
'volume_score': 0.15,
'volatility_score': 0.12,
'support_resistance_score': 0.10,
'multi_timeframe_score': 0.08, # 新增多时间框架分析
'strength_score': 0.05, # 新增趋势强度分析
'divergence_score': 0.05 # 新增背离分析
}
self.trading_analyzer = TradingSignalsAnalyzer() # 添加交易信号分析器
def calculate_score(self, stock_data: Dict) -> float:
try:
technical_indicators = stock_data.get('technical_indicators', {})
trend_score = self._score_trend(technical_indicators)
momentum_score = self._score_momentum(technical_indicators)
volume_score = self._score_volume(technical_indicators)
volatility_score = self._score_volatility(technical_indicators)
support_resistance_score = self._score_support_resistance(technical_indicators)
multi_timeframe_score = self._score_multi_timeframe(technical_indicators) # 新增
strength_score = self._score_trend_strength(technical_indicators) # 新增
divergence_score = self._score_price_volume_divergence(technical_indicators) # 新增
total_score = (
trend_score * self.weights['trend_score'] +
momentum_score * self.weights['momentum_score'] +
volume_score * self.weights['volume_score'] +
volatility_score * self.weights['volatility_score'] +
support_resistance_score * self.weights['support_resistance_score'] +
multi_timeframe_score * self.weights['multi_timeframe_score'] +
strength_score * self.weights['strength_score'] +
divergence_score * self.weights['divergence_score']
)
return total_score
except Exception as e:
print(f"Error in TechnicalStrategy.calculate_score: {e}")
return 0
def _score_trend(self, indicators: Dict) -> float:
"""趋势评分 - 增强容错性"""
try:
sma_20 = indicators.get('sma_20', 0)
sma_50 = indicators.get('sma_50', 0)
current_price = indicators.get('current_price', 0)
if current_price == 0 or sma_20 == 0 or sma_50 == 0:
return 50 # 默认中等分数
# 强势上升趋势
if current_price > sma_20 > sma_50:
return 90
# 中等上升趋势
elif current_price > sma_20:
return 70
# 弱势上升趋势
elif current_price > sma_50:
return 55
# 横盘整理
elif abs(current_price - sma_20) / current_price < 0.02:
return 50
# 弱势下降趋势
else:
return 30
except:
return 50
def _score_momentum(self, indicators: Dict) -> float:
"""动量评分 - 更宽松的RSI判断"""
try:
rsi = indicators.get('rsi', 50)
macd = indicators.get('macd', 0)
macd_signal = indicators.get('macd_signal', 0)
# RSI评分 - 更适合A股波动
if 40 <= rsi <= 65: # 健康区间
rsi_score = 80
elif 30 <= rsi <= 75: # 可接受区间
rsi_score = 60
elif rsi > 75: # 超买但仍可持有
rsi_score = 40
elif rsi < 30: # 超卖,潜在机会
rsi_score = 70
else:
rsi_score = 50
# MACD评分
macd_score = 65 if macd > macd_signal else 35
return (rsi_score + macd_score) / 2
except:
return 50
def _score_volume(self, indicators: Dict) -> float:
"""成交量评分 - 适应A股特点"""
try:
current_volume = indicators.get('current_volume', 0)
volume_sma = indicators.get('volume_sma', 0)
if volume_sma == 0 or current_volume == 0:
return 50
volume_ratio = current_volume / volume_sma
# 成交量放大评分
if volume_ratio >= 1.8: # 显著放量
return 85
elif volume_ratio >= 1.3: # 温和放量
return 70
elif volume_ratio >= 1.0: # 正常成交
return 60
elif volume_ratio >= 0.7: # 略微缩量
return 45
else: # 明显缩量
return 30
except:
return 50
def _score_volatility(self, indicators: Dict) -> float:
"""波动率评分 - 适应A股市场"""
try:
volatility = indicators.get('volatility', 20)
# A股适宜的波动率范围
if 8 <= volatility <= 25: # 理想波动率
return 80
elif 5 <= volatility <= 35: # 可接受波动率
return 65
elif volatility <= 45: # 较高波动率
return 50
else: # 过高波动率
return 30
except:
return 50
def _score_support_resistance(self, indicators: Dict) -> float:
"""支撑阻力评分 - 布林带位置"""
try:
current_price = indicators.get('current_price', 0)
bb_upper = indicators.get('bb_upper', 0)
bb_lower = indicators.get('bb_lower', 0)
bb_middle = indicators.get('bb_middle', 0)
if bb_upper == 0 or bb_lower == 0 or current_price == 0:
return 50
bb_position = (current_price - bb_lower) / (bb_upper - bb_lower)
# 布林带位置评分
if 0.4 <= bb_position <= 0.6: # 中轨附近,相对安全
return 75
elif 0.25 <= bb_position <= 0.75: # 合理区间
return 65
elif 0.15 <= bb_position <= 0.85: # 可接受区间
return 50
else: # 极端位置
return 35
except:
return 50
def _score_multi_timeframe(self, indicators: Dict) -> float:
"""多时间框架趋势一致性评分"""
try:
current_price = indicators.get('current_price', 0)
sma_20 = indicators.get('sma_20', 0)
sma_50 = indicators.get('sma_50', 0)
sma_200 = indicators.get('sma_200', 0)
if current_price == 0:
return 50
# 短中长期趋势一致性分析
short_trend = 1 if current_price > sma_20 else 0
medium_trend = 1 if sma_20 > sma_50 else 0
long_trend = 1 if sma_50 > sma_200 else 0
trend_alignment = short_trend + medium_trend + long_trend
if trend_alignment == 3: # 三重趋势向上
return 90
elif trend_alignment == 2: # 两重趋势向上
return 70
elif trend_alignment == 1: # 一重趋势向上
return 55
else: # 趋势向下
return 30
except:
return 50
def _score_trend_strength(self, indicators: Dict) -> float:
"""趋势强度评分 - ADX指标"""
try:
adx = indicators.get('adx', 25)
plus_di = indicators.get('plus_di', 20)
minus_di = indicators.get('minus_di', 20)
# ADX趋势强度判断
if adx >= 40: # 非常强的趋势
strength_score = 90
elif adx >= 25: # 强趋势
strength_score = 70
elif adx >= 15: # 中等趋势
strength_score = 50
else: # 弱趋势
strength_score = 30
# DI方向判断
if plus_di > minus_di: # 上升趋势
return strength_score
else: # 下降趋势
return max(strength_score - 20, 20)
except:
return 50
def _score_price_volume_divergence(self, indicators: Dict) -> float:
"""价量背离分析评分"""
try:
current_volume = indicators.get('current_volume', 0)
volume_sma = indicators.get('volume_sma', 0)
obv = indicators.get('obv', 0)
pvt = indicators.get('pvt', 0)
if current_volume == 0 or volume_sma == 0:
return 50
volume_ratio = current_volume / volume_sma
# 价量配合度分析
if volume_ratio >= 1.5: # 放量
if obv > 0 and pvt > 0: # 价量同步上涨
return 85
elif obv > 0 or pvt > 0: # 部分同步
return 65
else: # 价量背离
return 35
elif volume_ratio >= 1.0: # 正常量能
return 60
else: # 缩量
return 40
except:
return 50
def get_trading_signals(self, stock_data: Dict) -> Dict:
"""获取详细的交易信号分析"""
technical_indicators = stock_data.get('technical_indicators', {})
current_price = technical_indicators.get('current_price', 0)
# 计算技术分析综合评分
overall_score = self.calculate_score(stock_data)
# 分析买入信号
entry_signals = self.trading_analyzer.analyze_entry_signals(technical_indicators)
# 分析卖出信号
exit_signals = self.trading_analyzer.analyze_exit_signals(technical_indicators)
# 计算止盈止损
stop_loss_take_profit = self.trading_analyzer.calculate_stop_loss_take_profit(technical_indicators)
# 综合交易建议(考虑综合评分)
trading_advice = self._generate_trading_advice(entry_signals, exit_signals, stop_loss_take_profit, overall_score)
# 计算建议入场和出场价格
entry_price = self._calculate_entry_price(technical_indicators, entry_signals)
exit_price = self._calculate_exit_price(technical_indicators, exit_signals)
return {
'current_price': current_price,
'overall_score': overall_score,
'entry_signals': entry_signals,
'exit_signals': exit_signals,
'stop_loss_take_profit': stop_loss_take_profit,
'trading_advice': trading_advice,
'entry_price': entry_price,
'exit_price': exit_price,
'market_timing': self._assess_market_timing(technical_indicators)
}
def _generate_trading_advice(self, entry_signals: Dict, exit_signals: Dict, stp: Dict, overall_score: float = 50) -> Dict:
"""生成综合交易建议"""
advice = {
'action': 'WAIT',
'priority': 'LOW',
'reasoning': [],
'risk_assessment': 'MEDIUM',
'time_horizon': 'SHORT_TERM' # SHORT_TERM/MEDIUM_TERM/LONG_TERM
}
# 考虑综合评分对交易建议的影响
score_bias = 0
if overall_score >= 75:
score_bias = 2 # 高分偏向买入
advice['reasoning'].append(f'技术分析综合评分{overall_score:.1f}分,表现优异')
elif overall_score >= 60:
score_bias = 1 # 中等分数轻微偏向买入
advice['reasoning'].append(f'技术分析综合评分{overall_score:.1f}分,表现良好')
elif overall_score <= 40:
score_bias = -2 # 低分偏向卖出
advice['reasoning'].append(f'技术分析综合评分{overall_score:.1f}分,表现较差')
elif overall_score <= 50:
score_bias = -1 # 稍低分数轻微偏向观望
advice['reasoning'].append(f'技术分析综合评分{overall_score:.1f}分,表现一般')
# 基于买入信号强度(考虑评分调整)
entry_strength = 0
if entry_signals['overall_signal'] == 'STRONG_BUY':
entry_strength = 3
elif entry_signals['overall_signal'] == 'BUY':
entry_strength = 2
elif entry_signals['overall_signal'] == 'WEAK_BUY':
entry_strength = 1
elif entry_signals['overall_signal'] == 'NEUTRAL':
entry_strength = 0
# 基于卖出信号强度
exit_strength = 0
if exit_signals['overall_signal'] == 'STRONG_SELL':
exit_strength = -3
elif exit_signals['overall_signal'] == 'SELL':
exit_strength = -2
elif exit_signals['overall_signal'] == 'WEAK_SELL':
exit_strength = -1
# 综合判断(信号强度 + 评分偏向)
final_strength = entry_strength + exit_strength + score_bias
# 根据最终强度确定交易建议
if final_strength >= 3:
advice['action'] = 'BUY'
advice['priority'] = 'HIGH'
if entry_signals.get('entry_reasons'):
advice['reasoning'].extend(entry_signals['entry_reasons'])
elif final_strength >= 2:
advice['action'] = 'BUY'
advice['priority'] = 'MEDIUM'
if entry_signals.get('entry_reasons'):
advice['reasoning'].extend(entry_signals['entry_reasons'])
elif final_strength >= 1:
advice['action'] = 'CONSIDER_BUY'
advice['priority'] = 'LOW'
advice['reasoning'].append('综合信号偏向买入,但建议谨慎考虑')
elif final_strength <= -3:
advice['action'] = 'SELL'
advice['priority'] = 'HIGH'
if exit_signals.get('exit_reasons'):
advice['reasoning'].extend(exit_signals['exit_reasons'])
elif final_strength <= -2:
advice['action'] = 'SELL'
advice['priority'] = 'MEDIUM'
if exit_signals.get('exit_reasons'):
advice['reasoning'].extend(exit_signals['exit_reasons'])
else:
advice['action'] = 'WAIT'
advice['priority'] = 'LOW'
advice['reasoning'].append('当前信号不明确,建议继续观望')
# 调整风险评估
risk_factors = len(exit_signals.get('risk_flags', [])) + len(entry_signals.get('warning_flags', []))
if overall_score >= 70 and risk_factors == 0:
advice['risk_assessment'] = 'LOW'
elif overall_score >= 50 and risk_factors <= 1:
advice['risk_assessment'] = 'MEDIUM'
else:
advice['risk_assessment'] = 'HIGH'
# 时间框架建议
if stp.get('risk_reward_ratio', 0) >= 2:
advice['time_horizon'] = 'MEDIUM_TERM'
elif stp.get('risk_reward_ratio', 0) >= 1.5:
advice['time_horizon'] = 'SHORT_TERM'
else:
advice['time_horizon'] = 'INTRADAY'
return advice
def _assess_market_timing(self, indicators: Dict) -> Dict:
"""市场时机评估"""
volatility = indicators.get('volatility', 20)
volume_ratio = indicators.get('volume_ratio', 1)
adx = indicators.get('adx', 25)
timing = {
'market_condition': 'NORMAL',
'liquidity': 'GOOD',
'trend_strength': 'MEDIUM',
'optimal_for_trading': True,
'recommendations': []
}
# 市场状态评估
if volatility > 50:
timing['market_condition'] = 'VOLATILE'
timing['recommendations'].append('高波动环境,建议降低仓位')
elif volatility < 10:
timing['market_condition'] = 'QUIET'
timing['recommendations'].append('市场平静,适合区间操作')
# 流动性评估
if volume_ratio < 0.5:
timing['liquidity'] = 'POOR'
timing['optimal_for_trading'] = False
timing['recommendations'].append('成交量不足,不宜大额交易')
elif volume_ratio > 2:
timing['liquidity'] = 'EXCELLENT'
timing['recommendations'].append('成交活跃,适合短线操作')
# 趋势强度评估
if adx > 40:
timing['trend_strength'] = 'STRONG'
timing['recommendations'].append('强趋势环境,适合趋势跟随')
elif adx < 20:
timing['trend_strength'] = 'WEAK'
timing['recommendations'].append('趋势不明,建议区间操作')
return timing
def _calculate_entry_price(self, technical_indicators: Dict, entry_signals: Dict) -> Dict:
"""计算建议入场价格"""
current_price = technical_indicators.get('current_price', 0)
if current_price <= 0:
return {'recommended': None, 'reasoning': '无当前价格数据'}
sma_20 = technical_indicators.get('sma_20', 0)
bb_lower = technical_indicators.get('bb_lower', 0)
support_level = technical_indicators.get('support_level', 0)
# 根据信号强度确定入场价格策略
if entry_signals.get('overall_signal') == 'STRONG_BUY':
# 强买入信号:当前价格入场
return {
'recommended': round(current_price, 2),
'reasoning': '强买入信号,建议当前价格入场'
}
elif entry_signals.get('overall_signal') in ['BUY', 'WEAK_BUY']:
# 买入信号:寻找更好入场点
entry_candidates = [current_price]
# 如果价格接近支撑位,等待触及支撑
if bb_lower > 0 and current_price > bb_lower:
distance_to_support = (current_price - bb_lower) / current_price
if distance_to_support < 0.03: # 距离支撑位3%以内
entry_candidates.append(bb_lower * 1.005) # 支撑位上方0.5%
# 如果在20日均线上方不远等待回调
if sma_20 > 0 and current_price > sma_20:
distance_to_sma = (current_price - sma_20) / current_price
if distance_to_sma < 0.05: # 距离均线5%以内
entry_candidates.append(sma_20 * 1.01) # 均线上方1%
recommended_price = min(entry_candidates)
return {
'recommended': round(recommended_price, 2),
'reasoning': f'建议在{recommended_price:.2f}附近入场,等待更好买点'
}
return {'recommended': None, 'reasoning': '暂无买入信号'}
def _calculate_exit_price(self, technical_indicators: Dict, exit_signals: Dict) -> Dict:
"""计算建议出场价格"""
current_price = technical_indicators.get('current_price', 0)
if current_price <= 0:
return {'recommended': None, 'reasoning': '无当前价格数据'}
bb_upper = technical_indicators.get('bb_upper', 0)
resistance_level = technical_indicators.get('resistance_level', 0)
sma_20 = technical_indicators.get('sma_20', 0)
# 根据信号强度确定出场价格策略
if exit_signals.get('overall_signal') == 'STRONG_SELL':
# 强卖出信号:立即出场
return {
'recommended': round(current_price, 2),
'reasoning': '强卖出信号,建议立即出场'
}
elif exit_signals.get('overall_signal') in ['SELL', 'WEAK_SELL']:
# 卖出信号:寻找更好出场点
exit_candidates = [current_price]
# 如果接近阻力位,等待触及阻力位
if bb_upper > 0 and current_price < bb_upper:
distance_to_resistance = (bb_upper - current_price) / current_price
if distance_to_resistance < 0.03: # 距离阻力位3%以内
exit_candidates.append(bb_upper * 0.995) # 阻力位下方0.5%
# 如果在关键阻力位下方,等待反弹
if resistance_level > current_price > 0:
distance_to_resistance = (resistance_level - current_price) / current_price
if distance_to_resistance < 0.05: # 距离阻力位5%以内
exit_candidates.append(resistance_level * 0.99) # 阻力位下方1%
recommended_price = max(exit_candidates)
return {
'recommended': round(recommended_price, 2),
'reasoning': f'建议在{recommended_price:.2f}附近出场,等待更好卖点'
}
return {'recommended': None, 'reasoning': '暂无卖出信号'}
def get_criteria(self) -> Dict:
return {
'min_volume_ratio': 0.8,
'max_volatility': 60,
'rsi_range': [30, 70],
'description': '从沪深300成分股中寻找技术面良好、趋势向上、成交活跃的股票'
}

View File

@ -0,0 +1,179 @@
from .base_strategy import BaseStrategy
from typing import Dict
class ValueStrategy(BaseStrategy):
def __init__(self):
super().__init__(
name="价值投资策略",
description="基于低估值、高股息、稳定盈利的价值投资选股策略"
)
self.weights = {
'pe_score': 0.20,
'pb_score': 0.15,
'roe_score': 0.20,
'debt_score': 0.15,
'dividend_score': 0.08,
'margin_score': 0.10,
'cash_flow_score': 0.07, # 新增现金流质量
'asset_turnover_score': 0.05 # 新增资产效率
}
def calculate_score(self, stock_data: Dict) -> float:
try:
financial_ratios = stock_data.get('financial_ratios', {})
pe_ratio = financial_ratios.get('pe_ratio', float('inf'))
pb_ratio = financial_ratios.get('pb_ratio', float('inf'))
roe = financial_ratios.get('roe', 0)
debt_ratio = financial_ratios.get('debt_ratio', 100)
net_margin = financial_ratios.get('net_margin', 0)
dividend_yield = financial_ratios.get('dividend_yield', 0)
ocf_ratio = financial_ratios.get('ocf_ratio', 0) # 新增现金流比率
asset_turnover = financial_ratios.get('asset_turnover', 0) # 新增资产周转率
pe_score = self._score_pe_ratio(pe_ratio)
pb_score = self._score_pb_ratio(pb_ratio)
roe_score = self._score_roe(roe)
debt_score = self._score_debt_ratio(debt_ratio)
margin_score = self._score_net_margin(net_margin)
dividend_score = self._score_dividend_yield(dividend_yield)
cash_flow_score = self._score_cash_flow_quality(ocf_ratio) # 新增现金流评分
asset_turnover_score = self._score_asset_turnover(asset_turnover) # 新增效率评分
total_score = (
pe_score * self.weights['pe_score'] +
pb_score * self.weights['pb_score'] +
roe_score * self.weights['roe_score'] +
debt_score * self.weights['debt_score'] +
margin_score * self.weights['margin_score'] +
dividend_score * self.weights['dividend_score'] +
cash_flow_score * self.weights['cash_flow_score'] +
asset_turnover_score * self.weights['asset_turnover_score']
)
return total_score
except Exception as e:
print(f"Error in ValueStrategy.calculate_score: {e}")
return 0
def _score_pe_ratio(self, pe_ratio: float) -> float:
"""PE比率评分 - 适应A股市场"""
if pe_ratio <= 0 or pe_ratio == float('inf'):
return 0
elif pe_ratio <= 15: # 优秀估值
return 100
elif pe_ratio <= 25: # 合理估值
return 80
elif pe_ratio <= 45: # 偏高但可接受
return 60
elif pe_ratio <= 60: # 较高估值
return 40
else: # 过高估值
return 20
def _score_pb_ratio(self, pb_ratio: float) -> float:
"""PB比率评分 - 更现实的标准"""
if pb_ratio <= 0 or pb_ratio == float('inf'):
return 0
elif pb_ratio <= 1.5: # 低估值
return 100
elif pb_ratio <= 3.0: # 合理估值
return 80
elif pb_ratio <= 5.0: # 中等估值
return 60
elif pb_ratio <= 8.0: # 偏高估值
return 40
else: # 高估值
return 20
def _score_roe(self, roe: float) -> float:
"""ROE评分 - 符合A股实际情况"""
if roe >= 18: # 优秀盈利能力
return 100
elif roe >= 12: # 良好盈利能力
return 80
elif roe >= 8: # 中等盈利能力
return 60
elif roe >= 3: # 较低盈利能力
return 40
else: # 盈利能力差
return 20
def _score_debt_ratio(self, debt_ratio: float) -> float:
"""负债比率评分 - 调整为更宽松的标准"""
if debt_ratio <= 35: # 低负债
return 100
elif debt_ratio <= 55: # 中等负债
return 80
elif debt_ratio <= 75: # 偏高负债
return 60
elif debt_ratio <= 85: # 高负债
return 40
else: # 过高负债
return 20
def _score_net_margin(self, net_margin: float) -> float:
"""净利率评分 - 适应不同行业"""
if net_margin >= 12: # 高利润率
return 100
elif net_margin >= 8: # 较高利润率
return 80
elif net_margin >= 4: # 中等利润率
return 60
elif net_margin >= 1: # 较低利润率
return 40
else: # 低利润率
return 20
def _score_dividend_yield(self, dividend_yield: float) -> float:
"""股息率评分 - 符合A股分红水平"""
if dividend_yield >= 4: # 高股息
return 100
elif dividend_yield >= 2.5: # 较高股息
return 80
elif dividend_yield >= 1.5: # 中等股息
return 60
elif dividend_yield >= 0.5: # 较低股息
return 40
else: # 无或极低股息
return 20
def _score_cash_flow_quality(self, ocf_ratio: float) -> float:
"""现金流质量评分 - 经营现金流与净利润比率"""
if ocf_ratio >= 120: # 现金流远超净利润
return 100
elif ocf_ratio >= 100: # 现金流匹配净利润
return 90
elif ocf_ratio >= 80: # 现金流略低于净利润
return 70
elif ocf_ratio >= 60: # 现金流明显低于净利润
return 50
elif ocf_ratio >= 40: # 现金流质量一般
return 30
else: # 现金流质量差
return 10
def _score_asset_turnover(self, asset_turnover: float) -> float:
"""资产周转率评分 - 资产使用效率"""
if asset_turnover >= 1.5: # 高效率
return 100
elif asset_turnover >= 1.0: # 较高效率
return 80
elif asset_turnover >= 0.7: # 中等效率
return 60
elif asset_turnover >= 0.5: # 较低效率
return 40
else: # 低效率
return 20
def get_criteria(self) -> Dict:
return {
'min_pe': 35,
'max_pb': 3.0,
'min_roe': 5,
'max_debt_ratio': 80,
'min_net_margin': 2,
'description': '从沪深300成分股中寻找被低估、财务稳健、盈利能力强的价值股'
}

3
src/utils/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .helpers import setup_logger, safe_divide, format_number, get_current_date, validate_ts_code
__all__ = ['setup_logger', 'safe_divide', 'format_number', 'get_current_date', 'validate_ts_code']

54
src/utils/helpers.py Normal file
View File

@ -0,0 +1,54 @@
import logging
from datetime import datetime
def setup_logger(name: str, log_file: str = None, level: str = 'INFO') -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(getattr(logging, level.upper()))
if not logger.handlers:
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
if log_file:
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
def safe_divide(a: float, b: float, default: float = 0) -> float:
try:
return a / b if b != 0 else default
except (TypeError, ZeroDivisionError):
return default
def format_number(num: float, decimals: int = 2) -> str:
try:
if num == float('inf'):
return ''
elif num == float('-inf'):
return '-∞'
else:
return f"{num:.{decimals}f}"
except:
return "N/A"
def get_current_date() -> str:
return datetime.now().strftime('%Y%m%d')
def validate_ts_code(ts_code: str) -> bool:
if not ts_code or not isinstance(ts_code, str):
return False
parts = ts_code.split('.')
if len(parts) != 2:
return False
stock_code, exchange = parts
return (len(stock_code) == 6 and stock_code.isdigit() and
exchange in ['SH', 'SZ'])

23
start_web.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
echo "启动现代化股票分析平台..."
# 检查虚拟环境
if [ ! -d "venv" ]; then
echo "创建虚拟环境..."
python3 -m venv venv
fi
# 激活虚拟环境
echo "激活虚拟环境..."
source venv/bin/activate
# 安装依赖
echo "安装依赖..."
pip install -r web/requirements.txt
# 启动应用
echo "启动Web应用..."
echo "访问地址: http://localhost:5001"
echo "按 Ctrl+C 停止服务"
cd web && python app.py

108
web/README.md Normal file
View File

@ -0,0 +1,108 @@
# 现代化股票分析平台
一个基于Flask + HTML/CSS/JavaScript的现代化股票分析平台提供美观的用户界面和丰富的交互体验。
## ✨ 特性
- **🎨 现代化设计**: 玻璃拟态效果、渐变背景、流畅动画
- **📱 响应式布局**: 完美适配桌面和移动设备
- **⚡ 快速加载**: 轻量级架构,无需重型框架
- **🎯 交互丰富**: 实时图表、动态加载、键盘快捷键
- **🌙 深色模式**: 自动适配系统主题偏好
## 🚀 快速启动
### 方法一:使用启动脚本
```bash
./start_web.sh
```
### 方法二:手动启动
```bash
# 1. 激活虚拟环境
source venv/bin/activate
# 2. 安装依赖
pip install -r web/requirements.txt
# 3. 启动应用
cd web && python app.py
```
## 🌐 访问地址
启动后访问: [http://localhost:5001](http://localhost:5001)
## 📋 功能模块
### 1. 股票筛选
- 多种投资策略选择(价值投资、成长投资、技术分析)
- 可调节筛选条件(最低评分、结果数量)
- 实时图表展示(评分排行、行业分布)
### 2. 单股分析
- 股票代码输入验证
- 综合评分和投资建议
- 详细财务指标展示
- 雷达图可视化
### 3. 策略对比
- 多策略并行分析
- 对比结果可视化
- 最佳策略推荐
## ⌨️ 键盘快捷键
- `Ctrl + 1`: 切换到股票筛选
- `Ctrl + 2`: 切换到单股分析
- `Ctrl + 3`: 切换到策略对比
- `Ctrl + Enter`: 执行当前页面主要操作
## 🎨 设计特色
- **玻璃拟态**: 半透明背景 + 毛玻璃模糊效果
- **渐变主题**: 蓝紫色渐变配色方案
- **流畅动画**: 页面切换、加载状态、悬停效果
- **现代排版**: Inter 字体 + 合理的层次结构
- **智能提示**: Toast 通知 + 加载状态反馈
## 🔧 技术栈
- **前端**: HTML5 + CSS3 + Vanilla JavaScript
- **后端**: Flask (轻量级Python Web框架)
- **图表**: Chart.js
- **图标**: Font Awesome
- **字体**: Google Fonts (Inter)
## 📱 响应式适配
- **桌面**: 1200px+ (多列布局)
- **平板**: 768px-1199px (双列布局)
- **手机**: <768px (单列布局)
## 🎯 与Streamlit对比
| 特性 | Streamlit | 现代化平台 |
|------|-----------|------------|
| 设计自由度 | 受限 | 完全自定义 |
| 加载速度 | 较慢 | 快速 |
| 交互体验 | 基础 | 丰富 |
| 移动适配 | 一般 | 优秀 |
| 定制程度 | 低 | 高 |
## 🔄 项目结构
```
web/
├── app.py # Flask后端
├── requirements.txt # Python依赖
├── templates/
│ └── index.html # 主页面
└── static/
├── css/
│ └── style.css # 样式文件
└── js/
└── app.js # 交互脚本
```
现在你拥有了一个真正现代化的股票分析平台!🎉

35
web/app.py Normal file
View File

@ -0,0 +1,35 @@
from flask import Flask, render_template, jsonify, request
import requests
import json
app = Flask(__name__)
# API配置
API_BASE_URL = "http://localhost:8000/api"
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/screen', methods=['POST'])
def screen_stocks():
"""股票筛选代理接口"""
try:
data = request.json
response = requests.post(f"{API_BASE_URL}/screen", json=data)
return jsonify(response.json()), response.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/analyze', methods=['POST'])
def analyze_stock():
"""单股分析代理接口"""
try:
data = request.json
response = requests.post(f"{API_BASE_URL}/analyze", json=data)
return jsonify(response.json()), response.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, port=5001)

2
web/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Flask==2.3.3
requests==2.31.0

743
web/static/css/style.css Normal file
View File

@ -0,0 +1,743 @@
/* 全局重置和基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #000000; /* 纯黑色文字确保最高对比度 */
background: #f8f9fa; /* 浅灰背景增加层次感 */
min-height: 100vh;
}
/* 导航栏 */
.navbar {
background: #ffffff;
border-bottom: 1px solid #e5e5e5;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
padding: 0 2rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
height: 60px;
}
.nav-brand {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.4rem;
font-weight: 700;
color: #000000; /* 纯黑色确保清晰 */
}
.nav-menu {
display: flex;
gap: 1rem;
}
.nav-link {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
text-decoration: none;
color: #333333; /* 深灰色确保在白底上清晰可见 */
border-radius: 6px;
transition: all 0.2s ease;
font-weight: 500;
font-size: 0.9rem;
}
.nav-link:hover, .nav-link.active {
background: #f0f0f0;
color: #000000; /* 悬停时变为纯黑 */
}
/* 主容器 */
.main-container {
margin-top: 80px;
padding: 2rem;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
/* 页面标题 */
.page-header {
text-align: center;
margin-bottom: 3rem;
color: #000000; /* 标题用纯黑色 */
}
.page-header h1 {
font-size: 2.2rem;
font-weight: 600;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
color: #000000; /* 确保标题清晰 */
}
.page-header p {
font-size: 1rem;
color: #444444; /* 副标题用深灰色 */
max-width: 600px;
margin: 0 auto;
}
/* 卡片样式 */
.card {
background: #fafafa; /* 更浅的背景色 */
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); /* 更轻的阴影 */
border: 1px solid #f0f0f0; /* 更浅的边框 */
transition: box-shadow 0.2s ease;
}
.card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); /* 悬停时稍微深一点 */
}
.card-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #f0f0f0;
}
.card-header h3 {
font-size: 1.2rem;
font-weight: 600;
color: #000000; /* 卡片标题用纯黑色 */
}
/* 表单样式 */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-weight: 600;
color: #000000; /* 表单标签用纯黑色 */
margin-bottom: 6px;
font-size: 0.9rem;
}
.form-input, .form-select {
padding: 10px 12px;
border: 1px solid #ccc; /* 更深的边框确保可见 */
border-radius: 6px;
font-size: 0.9rem;
transition: border-color 0.2s ease;
background: #ffffff;
color: #000000; /* 输入文字用纯黑色 */
}
.form-input:focus, .form-select:focus {
outline: none;
border-color: #000000; /* 聚焦时黑色边框 */
}
.form-range {
width: 100%;
height: 8px;
border-radius: 5px;
background: #e2e8f0;
outline: none;
-webkit-appearance: none;
margin-bottom: 8px;
}
.form-range::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #000000; /* 纯黑色 */
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); /* 简单阴影 */
}
.form-range::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #000000; /* 纯黑色 */
cursor: pointer;
border: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); /* 简单阴影 */
}
.range-value {
font-weight: 600;
color: #000000; /* 纯黑色 */
text-align: center;
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
justify-content: center;
}
.btn-primary {
background: #1a1a1a;
color: #ffffff;
}
.btn-primary:hover {
background: #333333;
}
.btn-primary:active {
background: #0d0d0d;
}
/* 指标卡片 */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.metric-card {
background: #fafafa; /* 与主卡片保持一致的浅背景 */
border-radius: 8px;
padding: 1.5rem;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); /* 更轻的阴影 */
border: 1px solid #f0f0f0; /* 更浅的边框 */
transition: box-shadow 0.2s ease;
}
.metric-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); /* 悬停时稍微深一点 */
}
.metric-icon {
font-size: 1.8rem;
margin-bottom: 0.8rem;
}
.metric-value {
font-size: 1.6rem;
font-weight: 700;
color: #000000; /* 指标数值用纯黑色 */
margin-bottom: 0.3rem;
}
.metric-label {
color: #333333; /* 指标标签用深灰色 */
font-weight: 500;
font-size: 0.9rem;
}
/* 图表容器 */
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.chart-container {
background: #fafafa; /* 与其他卡片保持一致 */
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); /* 更轻的阴影 */
border: 1px solid #f0f0f0; /* 更浅的边框 */
}
/* 对比网格 */
.comparison-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* 表格样式 */
.data-table {
width: 100%;
border-collapse: collapse;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
margin-bottom: 2rem;
border: 1px solid #e5e5e5;
}
.data-table th {
background: #000000; /* 表头用纯黑背景 */
color: #ffffff;
padding: 12px 16px;
text-align: left;
font-weight: 600;
font-size: 0.9rem;
}
.data-table td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0; /* 更浅的分隔线 */
background: #fafafa; /* 与卡片背景一致 */
color: #000000; /* 表格内容用纯黑色 */
font-size: 0.9rem;
}
.data-table tr:hover td {
background: #f0f0f0; /* 悬停时稍微深一点 */
color: #000000;
}
/* 选项卡内容 */
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
.results-section {
animation: slideUp 0.5s ease;
}
/* 加载动画 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 2000;
color: #000000; /* 加载文字用纯黑色 */
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #e0e0e0;
border-top: 3px solid #000000; /* 旋转部分用黑色 */
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
/* Toast 通知 */
#toast-container {
position: fixed;
top: 100px;
right: 2rem;
z-index: 3000;
}
.toast {
background: #fafafa; /* 与卡片背景一致 */
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 稍微明显的阴影 */
border: 1px solid #f0f0f0; /* 与其他元素一致的边框 */
border-left: 3px solid #000000; /* 左边框用黑色 */
animation: slideInRight 0.3s ease;
max-width: 300px;
font-size: 0.9rem;
color: #000000; /* Toast文字用纯黑色 */
}
.toast.success {
border-left-color: #22c55e;
}
.toast.error {
border-left-color: #ef4444;
}
.toast.warning {
border-left-color: #f59e0b;
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
.navbar {
padding: 0 1rem;
}
.nav-container {
height: 60px;
}
.nav-brand {
font-size: 1.2rem;
}
.nav-menu {
gap: 1rem;
}
.nav-link {
padding: 8px 12px;
font-size: 0.9rem;
}
.main-container {
margin-top: 80px;
padding: 1rem;
}
.page-header h1 {
font-size: 2rem;
}
.page-header p {
font-size: 1rem;
}
.card {
padding: 1.5rem;
}
.form-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.charts-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.metrics-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.comparison-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
#toast-container {
right: 1rem;
left: 1rem;
top: 80px;
}
.toast {
max-width: none;
}
}
@media (max-width: 480px) {
.nav-menu {
flex-direction: column;
gap: 0.5rem;
}
.nav-link {
padding: 6px 10px;
font-size: 0.8rem;
}
.btn {
padding: 12px 24px;
font-size: 0.9rem;
}
.metric-value {
font-size: 1.5rem;
}
}
/* 综合分析页面特殊样式 */
.analysis-input-section {
display: flex;
align-items: flex-end;
gap: 1.5rem;
margin-bottom: 1.5rem;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 10px;
border: 1px solid #e9ecef;
}
.input-wrapper {
flex: 1;
min-width: 200px;
}
.input-wrapper label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #000000;
}
.input-hint {
display: block;
margin-top: 0.25rem;
color: #6c757d;
font-size: 0.875rem;
}
.analysis-btn {
padding: 12px 24px !important;
font-size: 1rem !important;
font-weight: 600 !important;
min-width: 180px;
height: auto;
white-space: nowrap;
margin-top: 0;
}
.analysis-info {
padding-top: 1rem;
border-top: 1px solid #e9ecef;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1rem;
}
.info-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: #ffffff;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.2s ease;
}
.info-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #000000;
}
.info-item i {
font-size: 1.2rem;
color: #000000;
width: 20px;
text-align: center;
}
.info-item span {
font-weight: 600;
color: #000000;
}
.analysis-desc {
text-align: center;
color: #6c757d;
font-size: 0.9rem;
margin: 0;
font-style: italic;
}
/* 响应式适配 */
@media (max-width: 768px) {
.analysis-input-section {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.input-wrapper {
flex: none;
}
.analysis-btn {
width: 100%;
}
.info-grid {
grid-template-columns: 1fr;
gap: 0.5rem;
}
}
/* 股票筛选页面特殊样式 */
.screening-info-main {
background: #f8f9fa;
border-radius: 10px;
padding: 1.5rem;
border: 1px solid #e9ecef;
}
.screening-criteria {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.criteria-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: #ffffff;
border-radius: 6px;
border: 1px solid #e9ecef;
font-weight: 500;
color: #000000;
}
.criteria-item i {
color: #007bff;
font-size: 1rem;
}
.strategy-overview {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid #e9ecef;
}
.strategy-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1rem;
}
.strategy-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.25rem;
background: #ffffff;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.2s ease;
}
.strategy-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #000000;
}
.strategy-item i {
font-size: 1.5rem;
color: #000000;
width: 24px;
text-align: center;
}
.strategy-info h4 {
margin: 0 0 0.25rem 0;
font-size: 1rem;
font-weight: 600;
color: #000000;
}
.strategy-info p {
margin: 0;
font-size: 0.875rem;
color: #6c757d;
}
@media (max-width: 768px) {
.screening-criteria {
flex-direction: column;
gap: 0.75rem;
align-items: center;
}
.criteria-item {
width: 100%;
justify-content: center;
}
.strategy-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.strategy-item {
padding: 1rem;
}
.strategy-item i {
font-size: 1.25rem;
}
}

862
web/static/js/app.js Normal file
View File

@ -0,0 +1,862 @@
// 全局状态管理
const AppState = {
currentTab: 'screening',
loading: false
};
// 全局图表实例管理
const ChartInstances = {
scoringChart: null,
industryChart: null
};
// DOM 加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
// 应用初始化
function initializeApp() {
setupNavigation();
setupEventListeners();
setupRangeSliders();
}
// 导航设置
function setupNavigation() {
const navLinks = document.querySelectorAll('.nav-link');
const tabContents = document.querySelectorAll('.tab-content');
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetTab = link.getAttribute('data-tab');
// 更新导航状态
navLinks.forEach(nav => nav.classList.remove('active'));
link.classList.add('active');
// 显示对应内容
tabContents.forEach(content => content.classList.remove('active'));
document.getElementById(targetTab).classList.add('active');
AppState.currentTab = targetTab;
});
});
}
// 事件监听器设置
function setupEventListeners() {
// 股票筛选
document.getElementById('screening-btn').addEventListener('click', handleScreening);
// 综合分析
document.getElementById('analysis-btn').addEventListener('click', handleComprehensiveAnalysis);
// 键盘事件
document.addEventListener('keydown', handleKeyPress);
}
// 范围滑块设置
function setupRangeSliders() {
// 移除范围滑块设置,因为筛选页面不再使用滑块
}
// 股票筛选处理
let isScreeningInProgress = false; // 防重复提交标志
async function handleScreening() {
// 防止重复点击
if (isScreeningInProgress) {
showToast('筛选正在进行中,请稍候...', 'warning');
return;
}
const minScore = 60; // 固定最低评分为60分
const limit = 50; // 固定结果数量为50个
// 综合筛选:使用三种策略综合评估
const data = { strategy: 'comprehensive', min_score: minScore, limit };
isScreeningInProgress = true;
const screeningBtn = document.getElementById('screening-btn');
const originalText = screeningBtn.innerHTML;
screeningBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 筛选中...';
screeningBtn.disabled = true;
showLoading('正在进行综合筛选...');
try {
const response = await fetch('/api/screen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
displayScreeningResults(result.results);
showToast('筛选成功!', 'success');
} else {
throw new Error(result.error || '筛选失败');
}
} catch (error) {
showToast(`筛选失败: ${error.message}`, 'error');
} finally {
isScreeningInProgress = false;
screeningBtn.innerHTML = originalText;
screeningBtn.disabled = false;
hideLoading();
}
}
// 显示筛选结果
function displayScreeningResults(results) {
const resultsSection = document.getElementById('screening-results');
const tableContainer = document.getElementById('screening-table');
if (!results || results.length === 0) {
tableContainer.innerHTML = '<p class="text-center">未找到符合条件的股票</p>';
resultsSection.style.display = 'block';
return;
}
// 检查是否是综合评分结果
const isComprehensive = results[0].value_score !== undefined;
// 创建表格
let headers, tableData;
if (isComprehensive) {
headers = ['股票代码', '名称', '行业', '综合评分', '价值评分', '成长评分', '技术评分', '建议'];
tableData = results.map(stock => [
stock.ts_code,
stock.name,
stock.industry,
stock.score.toFixed(1),
stock.value_score.toFixed(1),
stock.growth_score.toFixed(1),
stock.technical_score.toFixed(1),
stock.recommendation
]);
} else {
headers = ['股票代码', '名称', '行业', '评分', '建议'];
tableData = results.map(stock => [
stock.ts_code,
stock.name,
stock.industry,
stock.score.toFixed(1),
stock.recommendation
]);
}
const table = createDataTable(headers, tableData);
tableContainer.innerHTML = '';
tableContainer.appendChild(table);
// 创建图表
createScoringChart(results.slice(0, 10));
createIndustryChart(results);
resultsSection.style.display = 'block';
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
// 综合分析处理 - 合并多策略分析
async function handleComprehensiveAnalysis() {
const tsCodeInput = document.getElementById('stock-code').value.trim();
if (!tsCodeInput) {
showToast('请输入股票代码', 'warning');
return;
}
// 格式化股票代码:自动添加交易所后缀
const tsCode = formatStockCode(tsCodeInput);
const strategies = ['value', 'growth', 'technical'];
const results = [];
showLoading('正在进行综合分析...');
try {
// 并行获取三种策略的分析结果
for (const strategy of strategies) {
const response = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ts_code: tsCode, strategy })
});
const result = await response.json();
if (response.ok) {
results.push({
strategy: getStrategyName(strategy),
rawStrategy: strategy,
score: result.score,
recommendation: result.recommendation,
data: result
});
}
}
if (results.length > 0) {
displayComprehensiveResults(results, tsCode);
showToast('综合分析完成!', 'success');
} else {
throw new Error('无法获取分析数据');
}
} catch (error) {
showToast(`分析失败: ${error.message}`, 'error');
} finally {
hideLoading();
}
}
// 显示股票基本信息
function displayStockBasicInfo(results, tsCode) {
// 从价值策略结果中获取基本信息(因为价值策略通常有最完整的财务数据)
const valueData = results.find(r => r.rawStrategy === 'value') || results[0];
const stockData = valueData.data;
// 显示股票代码
document.getElementById('stock-code-value').textContent = tsCode;
// 显示股票名称
const stockName = stockData.name || stockData.stock_name || '--';
document.getElementById('stock-name-value').textContent = stockName;
// 显示行业信息
const industry = stockData.industry || '--';
document.getElementById('stock-industry-value').textContent = industry;
// 显示当前价格
const currentPrice = stockData.current_price || stockData.price;
if (currentPrice) {
document.getElementById('stock-price-value').textContent = `¥${currentPrice.toFixed(2)}`;
} else {
document.getElementById('stock-price-value').textContent = '--';
}
// 显示市值
const marketCap = stockData.market_cap;
if (marketCap) {
const marketCapBillion = (marketCap / 100000000).toFixed(2);
document.getElementById('stock-market-cap-value').textContent = `${marketCapBillion}亿`;
} else {
document.getElementById('stock-market-cap-value').textContent = '--';
}
// 显示市盈率
const peRatio = stockData.financial_ratios?.pe_ratio;
if (peRatio && peRatio !== Infinity && peRatio > 0) {
document.getElementById('stock-pe-value').textContent = peRatio.toFixed(2);
} else {
document.getElementById('stock-pe-value').textContent = '--';
}
}
// 显示综合分析结果
function displayComprehensiveResults(results, tsCode) {
const resultsSection = document.getElementById('analysis-results');
// 显示股票基本信息
displayStockBasicInfo(results, tsCode);
// 计算综合指标
const avgScore = results.reduce((sum, r) => sum + r.score, 0) / results.length;
const bestStrategy = results.reduce((best, current) =>
current.score > best.score ? current : best
);
// 综合建议逻辑
const buyCount = results.filter(r => r.recommendation === 'BUY').length;
const holdCount = results.filter(r => r.recommendation === 'HOLD').length;
let finalRecommendation;
let confidenceLevel;
if (buyCount >= 2) {
finalRecommendation = 'BUY';
confidenceLevel = buyCount === 3 ? 'HIGH' : 'MEDIUM';
} else if (buyCount + holdCount >= 2) {
finalRecommendation = 'HOLD';
confidenceLevel = 'MEDIUM';
} else {
finalRecommendation = 'SELL';
confidenceLevel = results.filter(r => r.recommendation === 'SELL').length >= 2 ? 'MEDIUM' : 'LOW';
}
// 显示综合投资评级
displayInvestmentRating(avgScore, bestStrategy, finalRecommendation, confidenceLevel);
// 显示分析总结(包含策略详情表格)
displayAnalysisSummary(results, avgScore, finalRecommendation, bestStrategy);
// 显示财务指标 (使用价值策略的数据)
const valueData = results.find(r => r.rawStrategy === 'value');
if (valueData && valueData.data.financial_ratios) {
displayFinancialRatios(valueData.data.financial_ratios);
}
// 检查是否有技术分析的交易信号
const technicalData = results.find(r => r.rawStrategy === 'technical');
if (technicalData && technicalData.data.trading_signals) {
displayTradingSignals(technicalData.data.trading_signals);
document.getElementById('trading-signals-section').style.display = 'block';
} else {
document.getElementById('trading-signals-section').style.display = 'none';
}
resultsSection.style.display = 'block';
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
// 显示综合投资评级
function displayInvestmentRating(avgScore, bestStrategy, finalRecommendation, confidenceLevel) {
// 显示综合评级指标
document.getElementById('best-strategy-value').textContent = bestStrategy.strategy;
document.getElementById('avg-score-value').textContent = avgScore.toFixed(1) + '分';
document.getElementById('final-recommendation-value').textContent = finalRecommendation;
document.getElementById('confidence-level-value').textContent =
confidenceLevel === 'HIGH' ? '高' :
confidenceLevel === 'MEDIUM' ? '中' : '低';
}
// 显示分析总结
function displayAnalysisSummary(results, avgScore, finalRecommendation, bestStrategy) {
const container = document.getElementById('summary-content');
let summaryHtml = `
<p><strong>综合评分</strong>${avgScore.toFixed(1)}</p>
<p><strong>投资建议</strong>${finalRecommendation}</p>
<p><strong>最佳策略</strong>${bestStrategy.strategy} (${bestStrategy.score.toFixed(1)})</p>
<br>
<p><strong>各策略分析</strong></p>
<ul>
`;
results.forEach(result => {
let analysis = '';
if (result.score >= 65) {
analysis = '表现优秀,符合投资标准';
} else if (result.score >= 45) {
analysis = '表现一般,可考虑关注';
} else {
analysis = '表现较差,需谨慎考虑';
}
summaryHtml += `<li><strong>${result.strategy}</strong>${result.score.toFixed(1)}分,${analysis}</li>`;
});
summaryHtml += '</ul>';
// 添加投资建议说明
if (finalRecommendation === 'BUY') {
summaryHtml += '<p style="color: #22c55e; margin-top: 1rem;"><strong>建议买入:</strong>多数策略看好,具有较好投资价值</p>';
} else if (finalRecommendation === 'HOLD') {
summaryHtml += '<p style="color: #f59e0b; margin-top: 1rem;"><strong>建议持有:</strong>策略结果分化,建议持有观望</p>';
} else {
summaryHtml += '<p style="color: #ef4444; margin-top: 1rem;"><strong>建议卖出:</strong>多数策略不看好,存在较大风险</p>';
}
// 添加策略详细评分表格
summaryHtml += `
<div style="margin-top: 1.5rem;">
<div id="strategy-comparison-table-inline"></div>
</div>
`;
container.innerHTML = summaryHtml;
// 创建策略对比表格
createStrategyComparisonTable(results, 'strategy-comparison-table-inline');
}
// 创建策略对比表格
function createStrategyComparisonTable(results, containerId = 'strategy-comparison-table') {
const container = document.getElementById(containerId);
const headers = ['投资策略', '评分', '投资建议', '策略特点'];
const rows = results.map(r => [
r.strategy,
r.score.toFixed(1) + '分',
r.recommendation,
getStrategyDescription(r.rawStrategy)
]);
const table = createDataTable(headers, rows);
container.innerHTML = '';
container.appendChild(table);
}
// 获取策略描述
function getStrategyDescription(strategy) {
const descriptions = {
'value': '注重估值合理性,适合稳健投资',
'growth': '关注成长潜力,适合追求高收益',
'technical': '基于技术指标,适合短期交易'
};
return descriptions[strategy] || '';
}
// 显示财务指标
function displayFinancialRatios(ratios) {
const container = document.getElementById('financial-ratios');
const ratioItems = [
{ key: 'roe', label: 'ROE (净资产收益率)', suffix: '%' },
{ key: 'roa', label: 'ROA (总资产收益率)', suffix: '%' },
{ key: 'gross_margin', label: '毛利率', suffix: '%' },
{ key: 'net_margin', label: '净利率', suffix: '%' },
{ key: 'current_ratio', label: '流动比率', suffix: '' },
{ key: 'debt_ratio', label: '负债比率', suffix: '%' }
];
const grid = document.createElement('div');
grid.className = 'metrics-grid';
ratioItems.forEach(item => {
if (ratios[item.key] !== undefined) {
const card = document.createElement('div');
card.className = 'metric-card';
card.innerHTML = `
<div class="metric-value">${ratios[item.key].toFixed(2)}${item.suffix}</div>
<div class="metric-label">${item.label}</div>
`;
grid.appendChild(card);
}
});
container.innerHTML = '';
container.appendChild(grid);
}
// 显示交易信号分析
function displayTradingSignals(tradingSignals) {
const entrySignals = tradingSignals.entry_signals || {};
const exitSignals = tradingSignals.exit_signals || {};
const stopLossTakeProfit = tradingSignals.stop_loss_take_profit || {};
const tradingAdvice = tradingSignals.trading_advice || {};
const marketTiming = tradingSignals.market_timing || {};
// 显示主要信号指标
document.getElementById('entry-signal-value').textContent =
getSignalDisplayText(entrySignals.overall_signal) + ` (${entrySignals.signal_strength || 0}%)`;
document.getElementById('exit-signal-value').textContent =
getSignalDisplayText(exitSignals.overall_signal) + ` (${exitSignals.signal_strength || 0}%)`;
document.getElementById('trading-action-value').textContent =
getActionDisplayText(tradingAdvice.action);
document.getElementById('risk-level-value').textContent =
getRiskDisplayText(tradingAdvice.risk_assessment);
// 显示买入价格和卖出价格建议
const currentPrice = tradingSignals.current_price || 0;
const entryPriceData = tradingSignals.entry_price || {};
const exitPriceData = tradingSignals.exit_price || {};
// 根据操作建议显示相应的价格
const action = tradingAdvice.action;
document.getElementById('entry-price-value').textContent =
(action === 'BUY' || action === 'CONSIDER_BUY') && entryPriceData.recommended ?
`¥${entryPriceData.recommended}` :
(action === 'BUY' || action === 'CONSIDER_BUY') && currentPrice > 0 ?
`¥${currentPrice.toFixed(2)}` : '--';
document.getElementById('exit-price-value').textContent =
(action === 'SELL') && exitPriceData.recommended ?
`¥${exitPriceData.recommended}` :
(action === 'SELL') && currentPrice > 0 ?
`¥${currentPrice.toFixed(2)}` : '--';
// 显示止盈止损信息(只有在非观望状态时才显示)
const stopLoss = stopLossTakeProfit.stop_loss || {};
const takeProfit = stopLossTakeProfit.take_profit || {};
const isWaitAction = tradingAdvice.action === 'WAIT';
document.getElementById('stop-loss-value').textContent =
(isWaitAction || !stopLoss.recommended) ? '--' : `¥${stopLoss.recommended}`;
document.getElementById('take-profit-value').textContent =
(isWaitAction || !takeProfit.recommended) ? '--' : `¥${takeProfit.recommended}`;
document.getElementById('risk-reward-ratio-value').textContent =
(isWaitAction || !stopLossTakeProfit.risk_reward_ratio) ? '--' : `1:${stopLossTakeProfit.risk_reward_ratio}`;
document.getElementById('position-size-value').textContent =
(isWaitAction || !stopLossTakeProfit.position_sizing_suggestion) ? '--' :
`${stopLossTakeProfit.position_sizing_suggestion.suggested_position}%`;
// 显示详细信号分析
displaySignalsDetails(entrySignals, exitSignals, tradingAdvice, marketTiming);
}
// 显示信号详情
function displaySignalsDetails(entrySignals, exitSignals, tradingAdvice, marketTiming) {
const container = document.getElementById('signals-content');
let detailsHtml = '<div class="signals-analysis">';
// 买入信号详情
if (entrySignals.entry_reasons && entrySignals.entry_reasons.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #22c55e; margin-bottom: 0.5rem;">📈 买入信号原因</h5>';
detailsHtml += '<ul>';
entrySignals.entry_reasons.forEach(reason => {
detailsHtml += `<li>${reason}</li>`;
});
detailsHtml += '</ul></div>';
}
// 卖出信号详情
if (exitSignals.exit_reasons && exitSignals.exit_reasons.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #ef4444; margin-bottom: 0.5rem;">📉 卖出信号原因</h5>';
detailsHtml += '<ul>';
exitSignals.exit_reasons.forEach(reason => {
detailsHtml += `<li>${reason}</li>`;
});
detailsHtml += '</ul></div>';
}
// 交易建议详情
if (tradingAdvice.reasoning && tradingAdvice.reasoning.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #3b82f6; margin-bottom: 0.5rem;">🎯 交易建议理由</h5>';
detailsHtml += '<ul>';
tradingAdvice.reasoning.forEach(reason => {
detailsHtml += `<li>${reason}</li>`;
});
detailsHtml += '</ul></div>';
}
// 市场时机分析
if (marketTiming.recommendations && marketTiming.recommendations.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #f59e0b; margin-bottom: 0.5rem;">⏰ 市场时机分析</h5>';
detailsHtml += `<p><strong>市场状态:</strong> ${getMarketConditionText(marketTiming.market_condition)}</p>`;
detailsHtml += `<p><strong>流动性:</strong> ${getLiquidityText(marketTiming.liquidity)}</p>`;
detailsHtml += `<p><strong>趋势强度:</strong> ${getTrendStrengthText(marketTiming.trend_strength)}</p>`;
detailsHtml += '<ul>';
marketTiming.recommendations.forEach(rec => {
detailsHtml += `<li>${rec}</li>`;
});
detailsHtml += '</ul></div>';
}
// 风险提示
const warningFlags = entrySignals.warning_flags || [];
const riskFlags = exitSignals.risk_flags || [];
if (warningFlags.length > 0 || riskFlags.length > 0) {
detailsHtml += '<div class="signal-section">';
detailsHtml += '<h5 style="color: #dc2626; margin-bottom: 0.5rem;">⚠️ 风险提示</h5>';
detailsHtml += '<ul>';
[...warningFlags, ...riskFlags].forEach(flag => {
detailsHtml += `<li style="color: #dc2626;">${flag}</li>`;
});
detailsHtml += '</ul></div>';
}
detailsHtml += '</div>';
container.innerHTML = detailsHtml;
}
// 信号显示文本转换函数
function getSignalDisplayText(signal) {
const signalMap = {
'STRONG_BUY': '强烈买入',
'BUY': '买入',
'WEAK_BUY': '弱买入',
'NEUTRAL': '中性',
'WEAK_SELL': '弱卖出',
'SELL': '卖出',
'STRONG_SELL': '强烈卖出'
};
return signalMap[signal] || '未知';
}
function getActionDisplayText(action) {
const actionMap = {
'BUY': '买入',
'SELL': '卖出',
'CONSIDER_BUY': '考虑买入',
'WAIT': '观望',
'HOLD': '持有'
};
return actionMap[action] || '观望';
}
function getRiskDisplayText(risk) {
const riskMap = {
'LOW': '低风险',
'MEDIUM': '中等风险',
'HIGH': '高风险'
};
return riskMap[risk] || '中等风险';
}
function getMarketConditionText(condition) {
const conditionMap = {
'NORMAL': '正常',
'VOLATILE': '高波动',
'QUIET': '平静'
};
return conditionMap[condition] || '正常';
}
function getLiquidityText(liquidity) {
const liquidityMap = {
'POOR': '流动性差',
'GOOD': '流动性良好',
'EXCELLENT': '流动性优秀'
};
return liquidityMap[liquidity] || '流动性良好';
}
function getTrendStrengthText(strength) {
const strengthMap = {
'WEAK': '趋势较弱',
'MEDIUM': '趋势中等',
'STRONG': '趋势较强'
};
return strengthMap[strength] || '趋势中等';
}
// 策略对比处理 - 已移除,合并到综合分析中
// 此函数已被 handleComprehensiveAnalysis 替代
// 显示对比结果 - 已移除,合并到综合分析中
// 此函数已被 displayComprehensiveResults 替代
// 工具函数
function formatStockCode(code) {
// 移除所有空格和非数字字符,保留原有后缀
const cleanCode = code.replace(/\s+/g, '').toUpperCase();
// 如果已经包含交易所后缀,直接返回
if (cleanCode.includes('.SZ') || cleanCode.includes('.SH')) {
return cleanCode;
}
// 只包含数字的情况,自动判断交易所
const numericCode = cleanCode.replace(/[^0-9]/g, '');
if (numericCode.length === 6) {
// 深交所000xxx, 002xxx, 300xxx
if (numericCode.startsWith('000') || numericCode.startsWith('002') || numericCode.startsWith('300')) {
return numericCode + '.SZ';
}
// 上交所600xxx, 601xxx, 603xxx, 688xxx
else if (numericCode.startsWith('60') || numericCode.startsWith('688')) {
return numericCode + '.SH';
}
}
// 默认返回原输入加.SZ深交所较多
return numericCode + '.SZ';
}
function getStrategyName(strategy) {
const names = {
'value': '价值投资',
'growth': '成长投资',
'technical': '技术分析'
};
return names[strategy] || strategy;
}
function getStrategyIcon(strategy) {
const icons = {
'价值投资': '💰',
'成长投资': '🚀',
'技术分析': '📈'
};
return icons[strategy] || '📊';
}
// 创建数据表格
function createDataTable(headers, rows) {
const table = document.createElement('table');
table.className = 'data-table';
// 创建表头
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
rows.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.textContent = cell;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
}
// 图表创建函数
function createScoringChart(data) {
const ctx = document.getElementById('scoring-chart').getContext('2d');
// 销毁已存在的图表
if (ChartInstances.scoringChart) {
ChartInstances.scoringChart.destroy();
}
ChartInstances.scoringChart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(item => item.name),
datasets: [{
label: '评分',
data: data.map(item => item.score),
backgroundColor: '#000000', /* 纯黑色 */
borderColor: '#000000',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: '股票评分排行' },
legend: { display: false }
},
scales: {
y: { beginAtZero: true, max: 100 }
}
}
});
}
function createIndustryChart(data) {
const ctx = document.getElementById('industry-chart').getContext('2d');
// 销毁已存在的图表
if (ChartInstances.industryChart) {
ChartInstances.industryChart.destroy();
}
const industries = {};
data.forEach(item => {
industries[item.industry] = (industries[item.industry] || 0) + 1;
});
ChartInstances.industryChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: Object.keys(industries),
datasets: [{
data: Object.values(industries),
backgroundColor: [
'#000000', '#333333', '#666666', '#999999',
'#cccccc', '#444444', '#777777', '#aaaaaa'
]
}]
},
options: {
responsive: true,
plugins: {
title: { display: true, text: '行业分布' }
}
}
});
}
// UI 辅助函数
function showLoading(message = '加载中...') {
const loading = document.getElementById('loading');
loading.querySelector('p').textContent = message;
loading.style.display = 'flex';
AppState.loading = true;
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
AppState.loading = false;
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icon = getToastIcon(type);
toast.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<span>${icon}</span>
<span>${message}</span>
</div>
`;
container.appendChild(toast);
// 自动移除
setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => container.removeChild(toast), 300);
}, 3000);
}
function getToastIcon(type) {
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: ''
};
return icons[type] || icons.info;
}
// 键盘快捷键处理
function handleKeyPress(e) {
if (AppState.loading) return;
// Ctrl + 数字键切换标签
if (e.ctrlKey) {
switch(e.key) {
case '1':
e.preventDefault();
document.querySelector('[data-tab="screening"]').click();
break;
case '2':
e.preventDefault();
document.querySelector('[data-tab="analysis"]').click();
break;
}
}
// Enter 键执行当前标签的主要操作
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault();
switch(AppState.currentTab) {
case 'screening':
document.getElementById('screening-btn').click();
break;
case 'analysis':
document.getElementById('analysis-btn').click();
break;
}
}
}

338
web/templates/index.html Normal file
View File

@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 金融分析平台</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-container">
<div class="nav-brand">
<i class="fas fa-chart-line"></i>
<span>AI 金融分析平台</span>
</div>
<div class="nav-menu">
<a href="#screening" class="nav-link active" data-tab="screening">
<i class="fas fa-filter"></i>
股票筛选
</a>
<a href="#analysis" class="nav-link" data-tab="analysis">
<i class="fas fa-chart-bar"></i>
综合分析
</a>
</div>
</div>
</nav>
<!-- 主容器 -->
<main class="main-container">
<!-- 股票筛选页面 -->
<section id="screening" class="tab-content active">
<div class="page-header">
<h1><i class="fas fa-filter"></i> 股票筛选</h1>
<p>运用专业投资策略从沪深300成分股中筛选出最具投资价值的标的</p>
</div>
<div class="card">
<div class="card-header">
<h3>筛选参数</h3>
</div>
<div class="card-body">
<div class="screening-info-main">
<div class="screening-criteria">
<div class="criteria-item">
<i class="fas fa-star"></i>
<span>综合评分 ≥ 60分</span>
</div>
<div class="criteria-item">
<i class="fas fa-chart-bar"></i>
<span>三维度综合评估</span>
</div>
<div class="criteria-item">
<i class="fas fa-list"></i>
<span>最多50只股票</span>
</div>
</div>
<div class="strategy-overview">
<div class="strategy-grid">
<div class="strategy-item">
<i class="fas fa-dollar-sign"></i>
<div class="strategy-info">
<h4>价值投资</h4>
<p>财务指标、估值分析</p>
</div>
</div>
<div class="strategy-item">
<i class="fas fa-rocket"></i>
<div class="strategy-info">
<h4>成长投资</h4>
<p>增长潜力、盈利能力</p>
</div>
</div>
<div class="strategy-item">
<i class="fas fa-chart-area"></i>
<div class="strategy-info">
<h4>技术分析</h4>
<p>趋势判断、交易时机</p>
</div>
</div>
</div>
</div>
</div>
<button id="screening-btn" class="btn btn-primary">
<i class="fas fa-search"></i>
开始智能筛选
</button>
</div>
</div>
<div id="screening-results" class="results-section" style="display: none;">
<div class="card">
<div class="card-header">
<h3>筛选结果</h3>
</div>
<div class="card-body">
<div id="screening-table"></div>
<div class="charts-grid">
<div class="chart-container">
<canvas id="scoring-chart"></canvas>
</div>
<div class="chart-container">
<canvas id="industry-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 综合分析页面 -->
<section id="analysis" class="tab-content">
<div class="page-header">
<h1><i class="fas fa-chart-bar"></i> 综合分析</h1>
<p>从价值投资、成长投资、技术分析三个维度全面评估股票投资价值</p>
</div>
<div class="card">
<div class="card-header">
<h3>分析参数</h3>
</div>
<div class="card-body">
<div class="analysis-input-section">
<div class="input-wrapper">
<label for="stock-code">股票代码</label>
<input type="text" id="stock-code" class="form-input" placeholder="如: 000001" value="000001">
<small class="input-hint">输入6位股票代码系统将自动识别交易所</small>
</div>
<button id="analysis-btn" class="btn btn-primary analysis-btn">
<i class="fas fa-chart-line"></i>
开始综合分析
</button>
</div>
<div class="analysis-info">
<div class="info-grid">
<div class="info-item">
<i class="fas fa-dollar-sign"></i>
<span>价值投资</span>
</div>
<div class="info-item">
<i class="fas fa-rocket"></i>
<span>成长投资</span>
</div>
<div class="info-item">
<i class="fas fa-chart-area"></i>
<span>技术分析</span>
</div>
</div>
<p class="analysis-desc">三维度专业分析,为您提供全面的投资决策支持</p>
</div>
</div>
</div>
<div id="analysis-results" class="results-section" style="display: none;">
<!-- 股票基本信息和财务指标 -->
<div class="card">
<div class="card-header">
<h3>股票基本信息</h3>
</div>
<div class="card-body">
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon">🏷️</div>
<div class="metric-value" id="stock-code-value">--</div>
<div class="metric-label">股票代码</div>
</div>
<div class="metric-card">
<div class="metric-icon">🏢</div>
<div class="metric-value" id="stock-name-value">--</div>
<div class="metric-label">股票名称</div>
</div>
<div class="metric-card">
<div class="metric-icon">🏭</div>
<div class="metric-value" id="stock-industry-value">--</div>
<div class="metric-label">所属行业</div>
</div>
<div class="metric-card">
<div class="metric-icon">💰</div>
<div class="metric-value" id="stock-price-value">--</div>
<div class="metric-label">当前价格</div>
</div>
<div class="metric-card">
<div class="metric-icon">📊</div>
<div class="metric-value" id="stock-market-cap-value">--</div>
<div class="metric-label">市值</div>
</div>
<div class="metric-card">
<div class="metric-icon">📈</div>
<div class="metric-value" id="stock-pe-value">--</div>
<div class="metric-label">市盈率</div>
</div>
</div>
<!-- 财务指标 -->
<div style="margin-top: 1.5rem;">
<h4>关键财务指标</h4>
<div id="financial-ratios"></div>
</div>
</div>
</div>
<!-- 综合投资评级 -->
<div class="card">
<div class="card-header">
<h3>综合投资评级</h3>
</div>
<div class="card-body">
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon">🎯</div>
<div class="metric-value" id="best-strategy-value">--</div>
<div class="metric-label">最佳策略</div>
</div>
<div class="metric-card">
<div class="metric-icon">📊</div>
<div class="metric-value" id="avg-score-value">--</div>
<div class="metric-label">综合评分</div>
</div>
<div class="metric-card">
<div class="metric-icon">💡</div>
<div class="metric-value" id="final-recommendation-value">--</div>
<div class="metric-label">投资建议</div>
</div>
<div class="metric-card">
<div class="metric-icon">📈</div>
<div class="metric-value" id="confidence-level-value">--</div>
<div class="metric-label">置信度</div>
</div>
</div>
<!-- 投资分析总结 -->
<div style="margin-top: 1.5rem;">
<h4>投资分析总结</h4>
<div id="summary-content"></div>
<!-- 策略评分详情表格 -->
<div style="margin-top: 1.5rem;">
<h5>各策略详细评分</h5>
<div id="strategy-comparison-table"></div>
</div>
</div>
</div>
</div>
<!-- 交易信号分析 (仅技术分析策略显示) -->
<div id="trading-signals-section" class="card" style="display: none;">
<div class="card-header">
<h3>交易时机分析</h3>
</div>
<div class="card-body">
<!-- 交易信号概览 -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon">📈</div>
<div class="metric-value" id="entry-signal-value">--</div>
<div class="metric-label">买入信号</div>
</div>
<div class="metric-card">
<div class="metric-icon">📉</div>
<div class="metric-value" id="exit-signal-value">--</div>
<div class="metric-label">卖出信号</div>
</div>
<div class="metric-card">
<div class="metric-icon">🎯</div>
<div class="metric-value" id="trading-action-value">--</div>
<div class="metric-label">操作建议</div>
</div>
<div class="metric-card">
<div class="metric-icon">⚠️</div>
<div class="metric-value" id="risk-level-value">--</div>
<div class="metric-label">风险评级</div>
</div>
<div class="metric-card">
<div class="metric-icon">💰</div>
<div class="metric-value" id="entry-price-value">--</div>
<div class="metric-label">建议买入价</div>
</div>
<div class="metric-card">
<div class="metric-icon">💸</div>
<div class="metric-value" id="exit-price-value">--</div>
<div class="metric-label">建议卖出价</div>
</div>
<div class="metric-card">
<div class="metric-icon">🛡️</div>
<div class="metric-value" id="stop-loss-value">--</div>
<div class="metric-label">建议止损</div>
</div>
<div class="metric-card">
<div class="metric-icon">🎯</div>
<div class="metric-value" id="take-profit-value">--</div>
<div class="metric-label">建议止盈</div>
</div>
<div class="metric-card">
<div class="metric-icon">⚖️</div>
<div class="metric-value" id="risk-reward-ratio-value">--</div>
<div class="metric-label">风险收益比</div>
</div>
<div class="metric-card">
<div class="metric-icon">📊</div>
<div class="metric-value" id="position-size-value">--</div>
<div class="metric-label">建议仓位</div>
</div>
</div>
<!-- 交易详情 -->
<div style="margin-top: 1.5rem;">
<h4>交易信号详情</h4>
<div id="trading-signals-details" class="card" style="margin-top: 0.5rem;">
<div class="card-body">
<div id="signals-content"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- 加载动画 -->
<div id="loading" class="loading-overlay" style="display: none;">
<div class="spinner"></div>
<p>正在处理中...</p>
</div>
<!-- Toast 通知 -->
<div id="toast-container"></div>
<!-- 脚本 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>