初始化提交:价值投资盯盘系统
This commit is contained in:
commit
01825eab57
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# 虚拟环境
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# 项目特定
|
||||||
|
config.json
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
64
README.md
Normal file
64
README.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# 价值投资盯盘系统
|
||||||
|
|
||||||
|
一个基于 Python FastAPI 开发的智能股票分析与监控平台。
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
- 实时股票数据监控
|
||||||
|
- AI 智能分析
|
||||||
|
- 财务指标分析
|
||||||
|
- 价值投资建议
|
||||||
|
- 股东信息查询
|
||||||
|
- 指数行情展示
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- 后端:FastAPI + Python
|
||||||
|
- 前端:Bootstrap 5 + ECharts
|
||||||
|
- 数据源:Tushare API
|
||||||
|
- 部署:Uvicorn
|
||||||
|
|
||||||
|
## 安装使用
|
||||||
|
|
||||||
|
1. 克隆项目
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/你的用户名/stock-monitor.git
|
||||||
|
cd stock-monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装依赖
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 配置
|
||||||
|
- 复制 `config.json.example` 为 `config.json`
|
||||||
|
- 在 `config.json` 中配置你的 Tushare Token
|
||||||
|
|
||||||
|
4. 运行
|
||||||
|
```bash
|
||||||
|
python run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 访问
|
||||||
|
打开浏览器访问 `http://localhost:8000`
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
主要配置项在 `app/config.py` 中:
|
||||||
|
- TUSHARE_TOKEN:Tushare API Token
|
||||||
|
- 其他配置项...
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
- 遵循 PEP 8 编码规范
|
||||||
|
- 使用 Python 3.8 或以上版本
|
||||||
|
- 保持代码简洁清晰
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
25
app/__init__.py
Normal file
25
app/__init__.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
import tushare as ts
|
||||||
|
from app.config import Config
|
||||||
|
|
||||||
|
# 确保必要的目录和文件存在
|
||||||
|
Config.ensure_directories()
|
||||||
|
|
||||||
|
# 创建FastAPI实例
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# 设置tushare token
|
||||||
|
ts.set_token(Config.TUSHARE_TOKEN)
|
||||||
|
pro = ts.pro_api()
|
||||||
|
|
||||||
|
# Mount static files
|
||||||
|
app.mount("/static", StaticFiles(directory=Config.STATIC_DIR), name="static")
|
||||||
|
|
||||||
|
# Set up templates
|
||||||
|
templates = Jinja2Templates(directory=Config.TEMPLATES_DIR)
|
||||||
|
|
||||||
|
# 导入路由
|
||||||
|
from app.api import stock_routes
|
||||||
|
app.include_router(stock_routes.router)
|
||||||
82
app/api/stock_routes.py
Normal file
82
app/api/stock_routes.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from fastapi import APIRouter, Request, Form
|
||||||
|
from typing import Optional
|
||||||
|
from app.services.stock_service import StockService
|
||||||
|
from app.services.ai_analysis_service import AIAnalysisService
|
||||||
|
from app import templates
|
||||||
|
|
||||||
|
router = APIRouter(prefix="")
|
||||||
|
stock_service = StockService()
|
||||||
|
ai_service = AIAnalysisService()
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def home(request: Request):
|
||||||
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|
||||||
|
@router.get("/api/stock_info/{stock_code}")
|
||||||
|
async def get_stock_info(stock_code: str, force_refresh: bool = False):
|
||||||
|
return stock_service.get_stock_info(stock_code, force_refresh)
|
||||||
|
|
||||||
|
@router.get("/api/watchlist")
|
||||||
|
async def get_watchlist():
|
||||||
|
return stock_service.get_watchlist()
|
||||||
|
|
||||||
|
@router.post("/api/add_watch")
|
||||||
|
async def add_watch(
|
||||||
|
stock_code: str = Form(...),
|
||||||
|
target_market_value_min: Optional[float] = Form(None),
|
||||||
|
target_market_value_max: Optional[float] = Form(None)
|
||||||
|
):
|
||||||
|
return stock_service.add_watch(stock_code, target_market_value_min, target_market_value_max)
|
||||||
|
|
||||||
|
@router.delete("/api/remove_watch/{stock_code}")
|
||||||
|
async def remove_watch(stock_code: str):
|
||||||
|
return stock_service.remove_watch(stock_code)
|
||||||
|
|
||||||
|
@router.get("/api/index_info")
|
||||||
|
async def get_index_info():
|
||||||
|
return stock_service.get_index_info()
|
||||||
|
|
||||||
|
@router.get("/market")
|
||||||
|
async def market(request: Request):
|
||||||
|
return templates.TemplateResponse("market.html", {"request": request})
|
||||||
|
|
||||||
|
@router.get("/api/company_detail/{stock_code}")
|
||||||
|
async def get_company_detail(stock_code: str):
|
||||||
|
return stock_service.get_company_detail(stock_code)
|
||||||
|
|
||||||
|
@router.get("/api/holders/{stock_code}")
|
||||||
|
async def get_top_holders(stock_code: str):
|
||||||
|
"""获取前十大股东数据"""
|
||||||
|
return stock_service.get_top_holders(stock_code)
|
||||||
|
|
||||||
|
@router.get("/api/performance_forecast/{stock_code}")
|
||||||
|
async def get_performance_forecast(stock_code: str):
|
||||||
|
"""获取业绩预告数据"""
|
||||||
|
# 处理股票代码格式
|
||||||
|
if stock_code.startswith('6'):
|
||||||
|
ts_code = f"{stock_code}.SH"
|
||||||
|
elif stock_code.startswith(('0', '3')):
|
||||||
|
ts_code = f"{stock_code}.SZ"
|
||||||
|
else:
|
||||||
|
return {"error": "不支持的股票代码"}
|
||||||
|
|
||||||
|
return stock_service.get_forecast_data(ts_code)
|
||||||
|
|
||||||
|
@router.get("/api/value_analysis/{stock_code}")
|
||||||
|
async def get_value_analysis(stock_code: str):
|
||||||
|
"""获取价值投资分析数据"""
|
||||||
|
return stock_service.get_value_analysis_data(stock_code)
|
||||||
|
|
||||||
|
@router.get("/api/ai_analysis/{stock_code}")
|
||||||
|
async def get_ai_analysis(stock_code: str):
|
||||||
|
"""获取AI价值投资分析结果"""
|
||||||
|
try:
|
||||||
|
# 首先获取价值分析数据
|
||||||
|
analysis_data = stock_service.get_value_analysis_data(stock_code)
|
||||||
|
if "error" in analysis_data:
|
||||||
|
return analysis_data
|
||||||
|
|
||||||
|
# 使用AI服务进行分析
|
||||||
|
return ai_service.analyze_value_investment(analysis_data)
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": f"AI分析失败: {str(e)}"}
|
||||||
29
app/config.py
Normal file
29
app/config.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# 基础配置
|
||||||
|
class Config:
|
||||||
|
# 项目根目录
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# Tushare API配置
|
||||||
|
TUSHARE_TOKEN = '90f8a141125e1decb952cd49032b7b8409a2d7fa370745f6c9f45c96'
|
||||||
|
|
||||||
|
# 配置文件路径
|
||||||
|
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
||||||
|
|
||||||
|
# 模板目录
|
||||||
|
TEMPLATES_DIR = os.path.join(BASE_DIR, "app", "templates")
|
||||||
|
|
||||||
|
# 静态文件目录
|
||||||
|
STATIC_DIR = os.path.join(BASE_DIR, "app", "static")
|
||||||
|
|
||||||
|
# 确保目录存在
|
||||||
|
@classmethod
|
||||||
|
def ensure_directories(cls):
|
||||||
|
os.makedirs(cls.STATIC_DIR, exist_ok=True)
|
||||||
|
os.makedirs(cls.TEMPLATES_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# 确保配置文件存在
|
||||||
|
if not os.path.exists(cls.CONFIG_FILE):
|
||||||
|
with open(cls.CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('{"watchlist": {}}')
|
||||||
32
app/models/stock.py
Normal file
32
app/models/stock.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Dict, Optional, Union, List
|
||||||
|
|
||||||
|
class MarketValueTarget(BaseModel):
|
||||||
|
min: Optional[float] = None
|
||||||
|
max: Optional[float] = None
|
||||||
|
|
||||||
|
class StockTarget(BaseModel):
|
||||||
|
target_market_value: Optional[MarketValueTarget] = None
|
||||||
|
|
||||||
|
class StockInfo(BaseModel):
|
||||||
|
code: str
|
||||||
|
name: str
|
||||||
|
market_value: float
|
||||||
|
pe_ratio: float
|
||||||
|
pb_ratio: float
|
||||||
|
ps_ratio: float
|
||||||
|
dividend_yield: float
|
||||||
|
price: float
|
||||||
|
change_percent: float
|
||||||
|
roe: float
|
||||||
|
gross_profit_margin: float
|
||||||
|
net_profit_margin: float
|
||||||
|
debt_to_assets: float
|
||||||
|
revenue_yoy: float
|
||||||
|
net_profit_yoy: float
|
||||||
|
bps: float
|
||||||
|
ocfps: float
|
||||||
|
|
||||||
|
class StockResponse(BaseModel):
|
||||||
|
stock_info: StockInfo
|
||||||
|
targets: StockTarget
|
||||||
308
app/services/ai_analysis_service.py
Normal file
308
app/services/ai_analysis_service.py
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
class AIAnalysisService:
|
||||||
|
def __init__(self):
|
||||||
|
self.model = "ep-20250111143839-vn8l8" # endpoint ID
|
||||||
|
self.client = OpenAI(
|
||||||
|
api_key = "cf4edd4d-55cd-4e0f-82f6-49072660bdaf", # 直接使用API Key
|
||||||
|
base_url = "https://ark.cn-beijing.volces.com/api/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
def analyze_value_investment(self, analysis_data):
|
||||||
|
"""
|
||||||
|
对股票进行价值投资分析
|
||||||
|
:param analysis_data: 包含各项财务指标的字典
|
||||||
|
:return: AI分析结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 打印输入数据用于调试
|
||||||
|
print(f"输入的分析数据: {json.dumps(analysis_data, ensure_ascii=False, indent=2)}")
|
||||||
|
|
||||||
|
# 构建提示词
|
||||||
|
prompt = self._build_analysis_prompt(analysis_data)
|
||||||
|
|
||||||
|
# 打印提示词用于调试
|
||||||
|
print(f"AI分析提示词: {prompt}")
|
||||||
|
|
||||||
|
# 调用API
|
||||||
|
response = self.client.chat.completions.create(
|
||||||
|
model=self.model,
|
||||||
|
messages=[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": prompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取分析结果
|
||||||
|
analysis_text = response.choices[0].message.content
|
||||||
|
print(f"AI原始返回结果: {analysis_text}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 尝试解析JSON
|
||||||
|
analysis_result = json.loads(analysis_text)
|
||||||
|
print(f"解析后的JSON结果: {json.dumps(analysis_result, ensure_ascii=False, indent=2)}")
|
||||||
|
|
||||||
|
# 构建完整的返回结果
|
||||||
|
result = analysis_result
|
||||||
|
|
||||||
|
print(f"最终返回结果: {json.dumps(result, ensure_ascii=False, indent=2)}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"JSON解析失败: {str(e)}")
|
||||||
|
# 如果JSON解析失败,返回错误信息
|
||||||
|
return {
|
||||||
|
'stock_info': analysis_data.get('stock_info', {}),
|
||||||
|
'valuation': analysis_data.get('valuation', {}),
|
||||||
|
'profitability': analysis_data.get('profitability', {}),
|
||||||
|
'growth': analysis_data.get('growth', {}),
|
||||||
|
'operation': analysis_data.get('operation', {}),
|
||||||
|
'solvency': analysis_data.get('solvency', {}),
|
||||||
|
'cash_flow': analysis_data.get('cash_flow', {}),
|
||||||
|
'per_share': analysis_data.get('per_share', {}),
|
||||||
|
'analysis_result': {
|
||||||
|
"error": "AI返回的结果不是有效的JSON格式",
|
||||||
|
"raw_text": analysis_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"AI分析失败: {str(e)}")
|
||||||
|
print(f"错误详情: {e.__class__.__name__}")
|
||||||
|
import traceback
|
||||||
|
print(f"错误堆栈: {traceback.format_exc()}")
|
||||||
|
return {"error": f"AI分析失败: {str(e)}"}
|
||||||
|
|
||||||
|
def _parse_analysis_result(self, analysis_text, current_price):
|
||||||
|
"""
|
||||||
|
解析AI返回的分析文本,提取结构化信息
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"开始解析分析文本...")
|
||||||
|
|
||||||
|
# 提取投资建议
|
||||||
|
suggestion_pattern = r"投资建议[::]([\s\S]*?)(?=\n\n|$)"
|
||||||
|
suggestion_match = re.search(suggestion_pattern, analysis_text, re.MULTILINE | re.DOTALL)
|
||||||
|
investment_suggestion = suggestion_match.group(1).strip() if suggestion_match else ""
|
||||||
|
print(f"提取到的投资建议: {investment_suggestion}")
|
||||||
|
|
||||||
|
# 提取合理价格区间
|
||||||
|
price_pattern = r"合理股价区间[::]\s*(\d+\.?\d*)\s*[元-]\s*(\d+\.?\d*)[元]"
|
||||||
|
price_match = re.search(price_pattern, analysis_text)
|
||||||
|
if price_match:
|
||||||
|
price_min = float(price_match.group(1))
|
||||||
|
price_max = float(price_match.group(2))
|
||||||
|
else:
|
||||||
|
price_min = current_price * 0.8
|
||||||
|
price_max = current_price * 1.2
|
||||||
|
print(f"提取到的价格区间: {price_min}-{price_max}")
|
||||||
|
|
||||||
|
# 提取目标市值区间(单位:亿元)
|
||||||
|
market_value_pattern = r"目标市值区间[::]\s*(\d+\.?\d*)\s*[亿-]\s*(\d+\.?\d*)[亿]"
|
||||||
|
market_value_match = re.search(market_value_pattern, analysis_text)
|
||||||
|
if market_value_match:
|
||||||
|
market_value_min = float(market_value_match.group(1))
|
||||||
|
market_value_max = float(market_value_match.group(2))
|
||||||
|
else:
|
||||||
|
# 尝试从文本中提取计算得出的市值
|
||||||
|
calc_pattern = r"最低市值[=≈约]*(\d+\.?\d*)[亿].*最高市值[=≈约]*(\d+\.?\d*)[亿]"
|
||||||
|
calc_match = re.search(calc_pattern, analysis_text)
|
||||||
|
if calc_match:
|
||||||
|
market_value_min = float(calc_match.group(1))
|
||||||
|
market_value_max = float(calc_match.group(2))
|
||||||
|
else:
|
||||||
|
market_value_min = 0
|
||||||
|
market_value_max = 0
|
||||||
|
print(f"提取到的市值区间: {market_value_min}-{market_value_max}")
|
||||||
|
|
||||||
|
# 提取各个分析维度的内容
|
||||||
|
analysis_patterns = {
|
||||||
|
"valuation_analysis": r"估值分析([\s\S]*?)(?=###\s*财务状况分析|###\s*成长性分析|$)",
|
||||||
|
"financial_health": r"财务状况分析([\s\S]*?)(?=###\s*成长性分析|###\s*风险评估|$)",
|
||||||
|
"growth_potential": r"成长性分析([\s\S]*?)(?=###\s*风险评估|###\s*投资建议|$)",
|
||||||
|
"risk_assessment": r"风险评估([\s\S]*?)(?=###\s*投资建议|$)"
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis_results = {}
|
||||||
|
for key, pattern in analysis_patterns.items():
|
||||||
|
match = re.search(pattern, analysis_text, re.MULTILINE | re.DOTALL)
|
||||||
|
content = match.group(1).strip() if match else ""
|
||||||
|
# 移除markdown标记和多余的空白字符
|
||||||
|
content = re.sub(r'[#\-*]', '', content).strip()
|
||||||
|
analysis_results[key] = content
|
||||||
|
print(f"提取到的{key}: {content[:100]}...")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"investment_suggestion": investment_suggestion,
|
||||||
|
"analysis": analysis_results,
|
||||||
|
"price_analysis": {
|
||||||
|
"reasonable_price_range": {
|
||||||
|
"min": price_min,
|
||||||
|
"max": price_max
|
||||||
|
},
|
||||||
|
"target_market_value": {
|
||||||
|
"min": market_value_min,
|
||||||
|
"max": market_value_max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"解析分析结果失败: {str(e)}")
|
||||||
|
print(f"错误详情: {e.__class__.__name__}")
|
||||||
|
import traceback
|
||||||
|
print(f"错误堆栈: {traceback.format_exc()}")
|
||||||
|
return {
|
||||||
|
"investment_suggestion": "分析结果解析失败",
|
||||||
|
"analysis": {
|
||||||
|
"valuation_analysis": "解析失败",
|
||||||
|
"financial_health": "解析失败",
|
||||||
|
"growth_potential": "解析失败",
|
||||||
|
"risk_assessment": "解析失败"
|
||||||
|
},
|
||||||
|
"price_analysis": {
|
||||||
|
"reasonable_price_range": {
|
||||||
|
"min": current_price * 0.8,
|
||||||
|
"max": current_price * 1.2
|
||||||
|
},
|
||||||
|
"target_market_value": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_analysis_prompt(self, data):
|
||||||
|
"""
|
||||||
|
构建AI分析提示词
|
||||||
|
"""
|
||||||
|
stock_info = data.get('stock_info', {})
|
||||||
|
valuation = data.get('valuation', {})
|
||||||
|
profitability = data.get('profitability', {})
|
||||||
|
growth = data.get('growth', {})
|
||||||
|
operation = data.get('operation', {})
|
||||||
|
solvency = data.get('solvency', {})
|
||||||
|
cash_flow = data.get('cash_flow', {})
|
||||||
|
per_share = data.get('per_share', {})
|
||||||
|
|
||||||
|
# 格式化数值,保留4位小数
|
||||||
|
def format_number(value):
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return "0.0000"
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
if abs(value) < 0.0001: # 对于非常小的数值
|
||||||
|
return "0.0000"
|
||||||
|
return f"{value:.4f}"
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
if abs(value) < 0.0001:
|
||||||
|
return "0.0000"
|
||||||
|
return f"{value:.4f}"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return str(value)
|
||||||
|
except:
|
||||||
|
return "0.0000"
|
||||||
|
|
||||||
|
# 格式化百分比,保留2位小数
|
||||||
|
def format_percent(value):
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return "0.00%"
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
# 如果值已经是小数形式(如0.5代表50%),则乘以100
|
||||||
|
if abs(value) <= 1:
|
||||||
|
value = value * 100
|
||||||
|
return f"{value:.2f}%"
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
if abs(value) <= 1:
|
||||||
|
value = value * 100
|
||||||
|
return f"{value:.2f}%"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return "0.00%"
|
||||||
|
except:
|
||||||
|
return "0.00%"
|
||||||
|
|
||||||
|
# 构建数据部分
|
||||||
|
data_section = f"""请作为一位专业的价值投资分析师,对{stock_info.get('name', '')}({stock_info.get('code', '')})进行深入的价值投资分析。
|
||||||
|
|
||||||
|
当前市场信息:
|
||||||
|
- 市盈率(PE):{format_number(valuation.get('pe_ratio'))}
|
||||||
|
- 市净率(PB):{format_number(valuation.get('pb_ratio'))}
|
||||||
|
- 市销率(PS):{format_number(valuation.get('ps_ratio'))}
|
||||||
|
- 股息率:{format_percent(valuation.get('dividend_yield'))}
|
||||||
|
- 总市值(亿元):{format_number(valuation.get('total_market_value'))}
|
||||||
|
- 当前股价:{format_number(stock_info.get('current_price'))}元
|
||||||
|
|
||||||
|
盈利能力指标:
|
||||||
|
- ROE:{format_percent(profitability.get('roe'))}
|
||||||
|
- 毛利率:{format_percent(profitability.get('gross_margin'))}
|
||||||
|
- 净利率:{format_percent(profitability.get('net_margin'))}
|
||||||
|
|
||||||
|
成长能力指标:
|
||||||
|
- 净利润增长率:{format_percent(growth.get('net_profit_growth'))}
|
||||||
|
- 扣非净利润增长率:{format_percent(growth.get('deducted_net_profit_growth'))}
|
||||||
|
- 营收增长率:{format_percent(growth.get('revenue_growth'))}
|
||||||
|
|
||||||
|
运营能力指标:
|
||||||
|
- 总资产周转率:{format_number(operation.get('asset_turnover'))}次/年
|
||||||
|
- 存货周转率:{format_number(operation.get('inventory_turnover'))}次/年
|
||||||
|
- 应收账款周转率:{format_number(operation.get('receivables_turnover'))}次/年
|
||||||
|
|
||||||
|
偿债能力指标:
|
||||||
|
- 流动比率:{format_number(solvency.get('current_ratio'))}
|
||||||
|
- 速动比率:{format_number(solvency.get('quick_ratio'))}
|
||||||
|
- 资产负债率:{format_percent(solvency.get('debt_to_assets'))}
|
||||||
|
|
||||||
|
现金流指标:
|
||||||
|
- 经营现金流/营收比:{format_percent(cash_flow.get('ocf_to_revenue'))}
|
||||||
|
- 经营现金流同比增长:{format_percent(cash_flow.get('ocf_growth'))}
|
||||||
|
|
||||||
|
每股指标:
|
||||||
|
- 每股收益(EPS):{format_number(per_share.get('eps'))}元
|
||||||
|
- 每股净资产(BPS):{format_number(per_share.get('bps'))}元
|
||||||
|
- 每股现金流(CFPS):{format_number(per_share.get('cfps'))}元
|
||||||
|
- 每股经营现金流(OCFPS):{format_number(per_share.get('ocfps'))}元
|
||||||
|
- 每股未分配利润:{format_number(per_share.get('retained_eps'))}元"""
|
||||||
|
|
||||||
|
# 构建分析要求部分
|
||||||
|
analysis_requirements = """
|
||||||
|
请基于以上数据,从价值投资的角度进行分析。请特别注意:
|
||||||
|
1. 结合行业特点、公司竞争力、成长性等因素,给出合理的估值区间
|
||||||
|
2. 存货周转率为0可能表示数据缺失,分析时需要谨慎对待
|
||||||
|
3. 考虑当前市场环境和行业整体估值水平
|
||||||
|
|
||||||
|
在给出估值区间时,请充分考虑:
|
||||||
|
1. 公司所处行业特点和竞争格局
|
||||||
|
2. 公司的竞争优势和市场地位
|
||||||
|
3. 当前的盈利能力和成长性
|
||||||
|
4. 财务健康状况和风险因素
|
||||||
|
5. 宏观经济环境和行业周期
|
||||||
|
6. 可比公司的估值水平
|
||||||
|
|
||||||
|
请以JSON格式返回分析结果,包含以下内容:
|
||||||
|
1. investment_suggestion: 投资建议,包含summary(总体建议)、action(具体操作建议)和key_points(关注重点)
|
||||||
|
2. analysis: 详细分析,包含估值分析、财务健康状况、成长潜力和风险评估
|
||||||
|
3. price_analysis: 价格分析,包含合理价格区间和目标市值区间
|
||||||
|
|
||||||
|
请确保返回的是一个有效的JSON格式,数值使用数字而不是字符串(价格、市值等),文本分析使用字符串。分析要客观、专业、详细。"""
|
||||||
|
|
||||||
|
# 组合完整的提示词
|
||||||
|
prompt = data_section + analysis_requirements
|
||||||
|
|
||||||
|
return prompt
|
||||||
573
app/services/stock_service.py
Normal file
573
app/services/stock_service.py
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import pandas as pd
|
||||||
|
from app import pro
|
||||||
|
from app.config import Config
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
class StockService:
|
||||||
|
def __init__(self):
|
||||||
|
self.watchlist = {}
|
||||||
|
self.cache_file = os.path.join(Config.BASE_DIR, "stock_cache.json")
|
||||||
|
self.load_watchlist()
|
||||||
|
self.load_cache()
|
||||||
|
|
||||||
|
def load_watchlist(self):
|
||||||
|
try:
|
||||||
|
if os.path.exists(Config.CONFIG_FILE):
|
||||||
|
with open(Config.CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.watchlist = data.get('watchlist', {})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading watchlist: {str(e)}")
|
||||||
|
self.watchlist = {}
|
||||||
|
|
||||||
|
def _save_watchlist(self):
|
||||||
|
try:
|
||||||
|
with open(Config.CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump({'watchlist': self.watchlist}, f, ensure_ascii=False, indent=4)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving watchlist: {str(e)}")
|
||||||
|
|
||||||
|
def load_cache(self):
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.cache_file):
|
||||||
|
with open(self.cache_file, 'r', encoding='utf-8') as f:
|
||||||
|
self.cache_data = json.load(f)
|
||||||
|
else:
|
||||||
|
self.cache_data = {}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading cache: {str(e)}")
|
||||||
|
self.cache_data = {}
|
||||||
|
|
||||||
|
def save_cache(self, stock_code, data):
|
||||||
|
try:
|
||||||
|
self.cache_data[stock_code] = {
|
||||||
|
'data': data,
|
||||||
|
'timestamp': datetime.now().strftime('%Y-%m-%d')
|
||||||
|
}
|
||||||
|
with open(self.cache_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.cache_data, f, ensure_ascii=False, indent=4)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving cache: {str(e)}")
|
||||||
|
|
||||||
|
def get_stock_info(self, stock_code: str, force_refresh: bool = False):
|
||||||
|
try:
|
||||||
|
# 检查缓存
|
||||||
|
today = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
if not force_refresh and stock_code in self.cache_data and self.cache_data[stock_code]['timestamp'] == today:
|
||||||
|
print(f"从缓存获取股票 {stock_code} 的数据")
|
||||||
|
cached_data = self.cache_data[stock_code]['data']
|
||||||
|
cached_data['stock_info']['from_cache'] = True
|
||||||
|
return cached_data
|
||||||
|
|
||||||
|
# 如果强制刷新或缓存不存在或已过期,从API获取数据
|
||||||
|
print(f"从API获取股票 {stock_code} 的数据...")
|
||||||
|
|
||||||
|
# 处理股票代码格式
|
||||||
|
if len(stock_code) != 6:
|
||||||
|
return {"error": "股票代码格式错误"}
|
||||||
|
|
||||||
|
# 确定交易所
|
||||||
|
if stock_code.startswith('6'):
|
||||||
|
ts_code = f"{stock_code}.SH"
|
||||||
|
elif stock_code.startswith(('0', '3')):
|
||||||
|
ts_code = f"{stock_code}.SZ"
|
||||||
|
else:
|
||||||
|
return {"error": "不支持的股票代码"}
|
||||||
|
|
||||||
|
# 获取基本信息和总市值
|
||||||
|
basic_info = pro.daily_basic(ts_code=ts_code, fields='ts_code,total_mv', limit=1)
|
||||||
|
if basic_info.empty:
|
||||||
|
return {"error": "股票代码不存在"}
|
||||||
|
|
||||||
|
# 获取股票名称
|
||||||
|
stock_name = pro.stock_basic(ts_code=ts_code, fields='name').iloc[0]['name']
|
||||||
|
|
||||||
|
# 获取最新财务指标
|
||||||
|
fina_indicator = pro.fina_indicator(ts_code=ts_code, period=datetime.now().strftime('%Y%m%d'), fields='roe,grossprofit_margin,netprofit_margin,debt_to_assets,op_income_yoy,netprofit_yoy,bps,ocfps')
|
||||||
|
if fina_indicator.empty:
|
||||||
|
fina_indicator = pro.fina_indicator(ts_code=ts_code, limit=1)
|
||||||
|
|
||||||
|
# 获取实时行情
|
||||||
|
today = datetime.now().strftime('%Y%m%d')
|
||||||
|
daily_data = pro.daily(ts_code=basic_info['ts_code'].iloc[0], start_date=today, end_date=today)
|
||||||
|
if daily_data.empty:
|
||||||
|
daily_data = pro.daily(ts_code=basic_info['ts_code'].iloc[0], limit=1)
|
||||||
|
if daily_data.empty:
|
||||||
|
return {"error": "无法获取股票行情数据"}
|
||||||
|
|
||||||
|
# 获取市值信息(用于其他指标)
|
||||||
|
daily_basic = pro.daily_basic(ts_code=basic_info['ts_code'].iloc[0],
|
||||||
|
fields='ts_code,trade_date,pe,pb,ps,dv_ratio',
|
||||||
|
limit=1)
|
||||||
|
|
||||||
|
if daily_basic.empty:
|
||||||
|
return {"error": "无法获取股票基础数据"}
|
||||||
|
|
||||||
|
latest_basic = daily_basic.iloc[0]
|
||||||
|
latest_fina = fina_indicator.iloc[0] if not fina_indicator.empty else pd.Series()
|
||||||
|
|
||||||
|
# 计算实时总市值(单位:亿元)
|
||||||
|
current_price = float(daily_data['close'].iloc[0])
|
||||||
|
market_value = float(basic_info['total_mv'].iloc[0]) / 10000 # 转换为亿元
|
||||||
|
print(f"市值计算: 当前价格={current_price}, 总市值={market_value}亿元")
|
||||||
|
|
||||||
|
# 处理股息率:tushare返回的是百分比值,需要转换为小数
|
||||||
|
dv_ratio = float(latest_basic['dv_ratio']) if pd.notna(latest_basic['dv_ratio']) else 0
|
||||||
|
dividend_yield = round(dv_ratio / 100, 4) # 转换为小数
|
||||||
|
|
||||||
|
# 处理财务指标,确保所有值都有默认值0,转换为小数
|
||||||
|
roe = round(float(latest_fina['roe']) / 100, 4) if pd.notna(latest_fina.get('roe')) else 0
|
||||||
|
gross_profit_margin = round(float(latest_fina['grossprofit_margin']) / 100, 4) if pd.notna(latest_fina.get('grossprofit_margin')) else 0
|
||||||
|
net_profit_margin = round(float(latest_fina['netprofit_margin']) / 100, 4) if pd.notna(latest_fina.get('netprofit_margin')) else 0
|
||||||
|
debt_to_assets = round(float(latest_fina['debt_to_assets']) / 100, 4) if pd.notna(latest_fina.get('debt_to_assets')) else 0
|
||||||
|
revenue_yoy = round(float(latest_fina['op_income_yoy']) / 100, 4) if pd.notna(latest_fina.get('op_income_yoy')) else 0
|
||||||
|
net_profit_yoy = round(float(latest_fina['netprofit_yoy']) / 100, 4) if pd.notna(latest_fina.get('netprofit_yoy')) else 0
|
||||||
|
bps = round(float(latest_fina['bps']), 3) if pd.notna(latest_fina.get('bps')) else 0 # 保留3位小数
|
||||||
|
ocfps = round(float(latest_fina['ocfps']), 3) if pd.notna(latest_fina.get('ocfps')) else 0 # 保留3位小数
|
||||||
|
|
||||||
|
stock_info = {
|
||||||
|
"code": stock_code,
|
||||||
|
"name": stock_name,
|
||||||
|
"market_value": round(market_value, 2), # 总市值(亿元)
|
||||||
|
"pe_ratio": round(float(latest_basic['pe']), 2) if pd.notna(latest_basic['pe']) else 0, # 市盈率
|
||||||
|
"pb_ratio": round(float(latest_basic['pb']), 2) if pd.notna(latest_basic['pb']) else 0, # 市净率
|
||||||
|
"ps_ratio": round(float(latest_basic['ps']), 2) if pd.notna(latest_basic['ps']) else 0, # 市销率
|
||||||
|
"dividend_yield": dividend_yield, # 股息率(小数)
|
||||||
|
"price": round(current_price, 2), # 股价保留2位小数
|
||||||
|
"change_percent": round(float(daily_data['pct_chg'].iloc[0]) / 100, 4), # 涨跌幅转换为小数
|
||||||
|
# 财务指标(全部转换为小数)
|
||||||
|
"roe": roe, # ROE(小数)
|
||||||
|
"gross_profit_margin": gross_profit_margin, # 毛利率(小数)
|
||||||
|
"net_profit_margin": net_profit_margin, # 净利率(小数)
|
||||||
|
"debt_to_assets": debt_to_assets, # 资产负债率(小数)
|
||||||
|
"revenue_yoy": revenue_yoy, # 营收增长率(小数)
|
||||||
|
"net_profit_yoy": net_profit_yoy, # 净利润增长率(小数)
|
||||||
|
"bps": bps, # 每股净资产
|
||||||
|
"ocfps": ocfps, # 每股经营现金流
|
||||||
|
"from_cache": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取目标值
|
||||||
|
targets = self.watchlist.get(stock_code, {})
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"stock_info": stock_info,
|
||||||
|
"targets": targets
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保存到缓存
|
||||||
|
self.save_cache(stock_code, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching stock info: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
print(f"详细错误: {traceback.format_exc()}")
|
||||||
|
return {"error": f"获取股票数据失败: {str(e)}"}
|
||||||
|
|
||||||
|
def get_watchlist(self):
|
||||||
|
result = []
|
||||||
|
for stock_code, targets in self.watchlist.items():
|
||||||
|
try:
|
||||||
|
# 从缓存获取数据
|
||||||
|
today = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
if stock_code in self.cache_data and self.cache_data[stock_code]['timestamp'] == today:
|
||||||
|
result.append(self.cache_data[stock_code]['data'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果没有缓存,只获取基本信息
|
||||||
|
if stock_code.startswith('6'):
|
||||||
|
ts_code = f"{stock_code}.SH"
|
||||||
|
elif stock_code.startswith(('0', '3')):
|
||||||
|
ts_code = f"{stock_code}.SZ"
|
||||||
|
else:
|
||||||
|
print(f"不支持的股票代码: {stock_code}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 获取股票名称
|
||||||
|
stock_name = pro.stock_basic(ts_code=ts_code, fields='name').iloc[0]['name']
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"stock_info": {
|
||||||
|
"code": stock_code,
|
||||||
|
"name": stock_name
|
||||||
|
},
|
||||||
|
"targets": targets
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting watchlist info for {stock_code}: {str(e)}")
|
||||||
|
continue
|
||||||
|
return result
|
||||||
|
|
||||||
|
def add_watch(self, stock_code: str, target_market_value_min: float = None, target_market_value_max: float = None):
|
||||||
|
self.watchlist[stock_code] = {
|
||||||
|
"target_market_value": {
|
||||||
|
"min": target_market_value_min,
|
||||||
|
"max": target_market_value_max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self._save_watchlist()
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
def remove_watch(self, stock_code: str):
|
||||||
|
if stock_code in self.watchlist:
|
||||||
|
del self.watchlist[stock_code]
|
||||||
|
# 同时删除缓存
|
||||||
|
if stock_code in self.cache_data:
|
||||||
|
del self.cache_data[stock_code]
|
||||||
|
try:
|
||||||
|
with open(self.cache_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.cache_data, f, ensure_ascii=False, indent=4)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving cache after removal: {str(e)}")
|
||||||
|
self._save_watchlist()
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
def get_index_info(self):
|
||||||
|
"""获取主要指数数据"""
|
||||||
|
try:
|
||||||
|
# 主要指数代码列表
|
||||||
|
index_codes = {
|
||||||
|
'000001.SH': '上证指数',
|
||||||
|
'399001.SZ': '深证成指',
|
||||||
|
'399006.SZ': '创业板指',
|
||||||
|
'000016.SH': '上证50',
|
||||||
|
'000300.SH': '沪深300',
|
||||||
|
'000905.SH': '中证500',
|
||||||
|
'000852.SH': '中证1000',
|
||||||
|
'899050.BJ': '北证50',
|
||||||
|
}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for ts_code, name in index_codes.items():
|
||||||
|
try:
|
||||||
|
# 获取指数基本信息
|
||||||
|
df = pro.index_daily(ts_code=ts_code, limit=1)
|
||||||
|
if not df.empty:
|
||||||
|
data = df.iloc[0]
|
||||||
|
# 获取K线数据(最近20天)
|
||||||
|
kline_df = pro.index_daily(ts_code=ts_code, limit=20)
|
||||||
|
kline_data = []
|
||||||
|
if not kline_df.empty:
|
||||||
|
for _, row in kline_df.iterrows():
|
||||||
|
kline_data.append({
|
||||||
|
'date': row['trade_date'],
|
||||||
|
'open': float(row['open']),
|
||||||
|
'close': float(row['close']),
|
||||||
|
'high': float(row['high']),
|
||||||
|
'low': float(row['low']),
|
||||||
|
'vol': float(row['vol'])
|
||||||
|
})
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'code': ts_code,
|
||||||
|
'name': name,
|
||||||
|
'price': float(data['close']),
|
||||||
|
'change': float(data['pct_chg']),
|
||||||
|
'kline_data': kline_data
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取指数 {ts_code} 数据失败: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取指数数据失败: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_company_detail(self, stock_code: str):
|
||||||
|
try:
|
||||||
|
print(f"开始获取公司详情: {stock_code}")
|
||||||
|
|
||||||
|
# 处理股票代码格式
|
||||||
|
if stock_code.startswith('6'):
|
||||||
|
ts_code = f"{stock_code}.SH"
|
||||||
|
elif stock_code.startswith(('0', '3')):
|
||||||
|
ts_code = f"{stock_code}.SZ"
|
||||||
|
else:
|
||||||
|
print(f"不支持的股票代码格式: {stock_code}")
|
||||||
|
return {"error": "不支持的股票代码"}
|
||||||
|
|
||||||
|
print(f"转换后的ts_code: {ts_code}")
|
||||||
|
|
||||||
|
# 获取公司基本信息
|
||||||
|
basic = pro.stock_basic(ts_code=ts_code, fields='name,industry,area,list_date')
|
||||||
|
if basic.empty:
|
||||||
|
print(f"无法获取公司基本信息: {ts_code}")
|
||||||
|
return {"error": "无法获取公司信息"}
|
||||||
|
|
||||||
|
company_info = basic.iloc[0]
|
||||||
|
print(f"获取到的公司基本信息: {company_info.to_dict()}")
|
||||||
|
|
||||||
|
# 获取公司详细信息
|
||||||
|
try:
|
||||||
|
company_detail = pro.stock_company(ts_code=ts_code)
|
||||||
|
if not company_detail.empty:
|
||||||
|
detail_info = company_detail.iloc[0]
|
||||||
|
company_detail_dict = {
|
||||||
|
"com_name": str(detail_info.get('com_name', '')),
|
||||||
|
"chairman": str(detail_info.get('chairman', '')),
|
||||||
|
"manager": str(detail_info.get('manager', '')),
|
||||||
|
"secretary": str(detail_info.get('secretary', '')),
|
||||||
|
"reg_capital": float(detail_info.get('reg_capital', 0)) if pd.notna(detail_info.get('reg_capital')) else 0,
|
||||||
|
"setup_date": str(detail_info.get('setup_date', '')),
|
||||||
|
"province": str(detail_info.get('province', '')),
|
||||||
|
"city": str(detail_info.get('city', '')),
|
||||||
|
"introduction": str(detail_info.get('introduction', '')),
|
||||||
|
"website": f"http://{str(detail_info.get('website', '')).strip('http://').strip('https://')}" if detail_info.get('website') else "",
|
||||||
|
"email": str(detail_info.get('email', '')),
|
||||||
|
"office": str(detail_info.get('office', '')),
|
||||||
|
"employees": int(detail_info.get('employees', 0)) if pd.notna(detail_info.get('employees')) else 0,
|
||||||
|
"main_business": str(detail_info.get('main_business', '')),
|
||||||
|
"business_scope": str(detail_info.get('business_scope', ''))
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
company_detail_dict = {
|
||||||
|
"com_name": "", "chairman": "", "manager": "", "secretary": "",
|
||||||
|
"reg_capital": 0, "setup_date": "", "province": "", "city": "",
|
||||||
|
"introduction": "", "website": "", "email": "", "office": "",
|
||||||
|
"employees": 0, "main_business": "", "business_scope": ""
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取公司详细信息失败: {str(e)}")
|
||||||
|
company_detail_dict = {
|
||||||
|
"com_name": "", "chairman": "", "manager": "", "secretary": "",
|
||||||
|
"reg_capital": 0, "setup_date": "", "province": "", "city": "",
|
||||||
|
"introduction": "", "website": "", "email": "", "office": "",
|
||||||
|
"employees": 0, "main_business": "", "business_scope": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取最新财务指标
|
||||||
|
try:
|
||||||
|
fina = pro.fina_indicator(ts_code=ts_code, period=datetime.now().strftime('%Y%m%d'))
|
||||||
|
if fina.empty:
|
||||||
|
print("当前期间无财务数据,尝试获取最新一期数据")
|
||||||
|
fina = pro.fina_indicator(ts_code=ts_code, limit=1)
|
||||||
|
|
||||||
|
if fina.empty:
|
||||||
|
print(f"无法获取财务指标数据: {ts_code}")
|
||||||
|
return {"error": "无法获取财务数据"}
|
||||||
|
|
||||||
|
fina_info = fina.iloc[0]
|
||||||
|
print(f"获取到的财务指标: {fina_info.to_dict()}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取财务指标失败: {str(e)}")
|
||||||
|
return {"error": "获取财务指标失败"}
|
||||||
|
|
||||||
|
# 获取市值信息(用于PE、PB等指标)
|
||||||
|
try:
|
||||||
|
daily_basic = pro.daily_basic(ts_code=ts_code, fields='pe,pb,ps,dv_ratio', limit=1)
|
||||||
|
if not daily_basic.empty:
|
||||||
|
latest_basic = daily_basic.iloc[0]
|
||||||
|
else:
|
||||||
|
print("无法获取PE/PB数据")
|
||||||
|
latest_basic = pd.Series({'pe': 0, 'pb': 0, 'ps': 0, 'dv_ratio': 0})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取PE/PB失败: {str(e)}")
|
||||||
|
latest_basic = pd.Series({'pe': 0, 'pb': 0, 'ps': 0, 'dv_ratio': 0})
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"basic_info": {
|
||||||
|
"name": str(company_info['name']),
|
||||||
|
"industry": str(company_info['industry']),
|
||||||
|
"list_date": str(company_info['list_date']),
|
||||||
|
"area": str(company_info['area']),
|
||||||
|
**company_detail_dict
|
||||||
|
},
|
||||||
|
"financial_info": {
|
||||||
|
# 估值指标
|
||||||
|
"pe_ratio": float(latest_basic['pe']) if pd.notna(latest_basic['pe']) else 0,
|
||||||
|
"pb_ratio": float(latest_basic['pb']) if pd.notna(latest_basic['pb']) else 0,
|
||||||
|
"ps_ratio": float(latest_basic['ps']) if pd.notna(latest_basic['ps']) else 0,
|
||||||
|
"dividend_yield": float(latest_basic['dv_ratio'])/100 if pd.notna(latest_basic['dv_ratio']) else 0,
|
||||||
|
|
||||||
|
# 盈利能力
|
||||||
|
"roe": float(fina_info['roe']) if pd.notna(fina_info.get('roe')) else 0,
|
||||||
|
"roe_dt": float(fina_info['roe_dt']) if pd.notna(fina_info.get('roe_dt')) else 0,
|
||||||
|
"roa": float(fina_info['roa']) if pd.notna(fina_info.get('roa')) else 0,
|
||||||
|
"grossprofit_margin": float(fina_info['grossprofit_margin']) if pd.notna(fina_info.get('grossprofit_margin')) else 0,
|
||||||
|
"netprofit_margin": float(fina_info['netprofit_margin']) if pd.notna(fina_info.get('netprofit_margin')) else 0,
|
||||||
|
|
||||||
|
# 成长能力
|
||||||
|
"netprofit_yoy": float(fina_info['netprofit_yoy']) if pd.notna(fina_info.get('netprofit_yoy')) else 0,
|
||||||
|
"dt_netprofit_yoy": float(fina_info['dt_netprofit_yoy']) if pd.notna(fina_info.get('dt_netprofit_yoy')) else 0,
|
||||||
|
"tr_yoy": float(fina_info['tr_yoy']) if pd.notna(fina_info.get('tr_yoy')) else 0,
|
||||||
|
"or_yoy": float(fina_info['or_yoy']) if pd.notna(fina_info.get('or_yoy')) else 0,
|
||||||
|
|
||||||
|
# 营运能力
|
||||||
|
"assets_turn": float(fina_info['assets_turn']) if pd.notna(fina_info.get('assets_turn')) else 0,
|
||||||
|
"inv_turn": float(fina_info['inv_turn']) if pd.notna(fina_info.get('inv_turn')) else 0,
|
||||||
|
"ar_turn": float(fina_info['ar_turn']) if pd.notna(fina_info.get('ar_turn')) else 0,
|
||||||
|
"ca_turn": float(fina_info['ca_turn']) if pd.notna(fina_info.get('ca_turn')) else 0,
|
||||||
|
|
||||||
|
# 偿债能力
|
||||||
|
"current_ratio": float(fina_info['current_ratio']) if pd.notna(fina_info.get('current_ratio')) else 0,
|
||||||
|
"quick_ratio": float(fina_info['quick_ratio']) if pd.notna(fina_info.get('quick_ratio')) else 0,
|
||||||
|
"debt_to_assets": float(fina_info['debt_to_assets']) if pd.notna(fina_info.get('debt_to_assets')) else 0,
|
||||||
|
"debt_to_eqt": float(fina_info['debt_to_eqt']) if pd.notna(fina_info.get('debt_to_eqt')) else 0,
|
||||||
|
|
||||||
|
# 现金流
|
||||||
|
"ocf_to_or": float(fina_info['ocf_to_or']) if pd.notna(fina_info.get('ocf_to_or')) else 0,
|
||||||
|
"ocf_to_opincome": float(fina_info['ocf_to_opincome']) if pd.notna(fina_info.get('ocf_to_opincome')) else 0,
|
||||||
|
"ocf_yoy": float(fina_info['ocf_yoy']) if pd.notna(fina_info.get('ocf_yoy')) else 0,
|
||||||
|
|
||||||
|
# 每股指标
|
||||||
|
"eps": float(fina_info['eps']) if pd.notna(fina_info.get('eps')) else 0,
|
||||||
|
"dt_eps": float(fina_info['dt_eps']) if pd.notna(fina_info.get('dt_eps')) else 0,
|
||||||
|
"bps": float(fina_info['bps']) if pd.notna(fina_info.get('bps')) else 0,
|
||||||
|
"ocfps": float(fina_info['ocfps']) if pd.notna(fina_info.get('ocfps')) else 0,
|
||||||
|
"retainedps": float(fina_info['retainedps']) if pd.notna(fina_info.get('retainedps')) else 0,
|
||||||
|
"cfps": float(fina_info['cfps']) if pd.notna(fina_info.get('cfps')) else 0,
|
||||||
|
"ebit_ps": float(fina_info['ebit_ps']) if pd.notna(fina_info.get('ebit_ps')) else 0,
|
||||||
|
"fcff_ps": float(fina_info['fcff_ps']) if pd.notna(fina_info.get('fcff_ps')) else 0,
|
||||||
|
"fcfe_ps": float(fina_info['fcfe_ps']) if pd.notna(fina_info.get('fcfe_ps')) else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"返回结果: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting company detail: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
print(f"详细错误: {traceback.format_exc()}")
|
||||||
|
return {"error": f"获取公司详情失败: {str(e)}"}
|
||||||
|
|
||||||
|
def get_top_holders(self, stock_code: str):
|
||||||
|
"""获取前十大股东数据"""
|
||||||
|
try:
|
||||||
|
# 处理股票代码格式
|
||||||
|
if stock_code.startswith('6'):
|
||||||
|
ts_code = f"{stock_code}.SH"
|
||||||
|
elif stock_code.startswith(('0', '3')):
|
||||||
|
ts_code = f"{stock_code}.SZ"
|
||||||
|
else:
|
||||||
|
return {"error": "不支持的股票代码"}
|
||||||
|
|
||||||
|
# 获取最新一期的股东数据
|
||||||
|
df = pro.top10_holders(ts_code=ts_code, limit=10)
|
||||||
|
if df.empty:
|
||||||
|
return {"error": "暂无股东数据"}
|
||||||
|
|
||||||
|
# 按持股比例降序排序
|
||||||
|
df = df.sort_values('hold_ratio', ascending=False)
|
||||||
|
|
||||||
|
# 获取最新的报告期
|
||||||
|
latest_end_date = df['end_date'].max()
|
||||||
|
latest_data = df[df['end_date'] == latest_end_date]
|
||||||
|
|
||||||
|
holders = []
|
||||||
|
for _, row in latest_data.iterrows():
|
||||||
|
holders.append({
|
||||||
|
"holder_name": str(row['holder_name']),
|
||||||
|
"hold_amount": float(row['hold_amount']) if pd.notna(row['hold_amount']) else 0,
|
||||||
|
"hold_ratio": float(row['hold_ratio']) if pd.notna(row['hold_ratio']) else 0,
|
||||||
|
"hold_change": float(row['hold_change']) if pd.notna(row['hold_change']) else 0,
|
||||||
|
"ann_date": str(row['ann_date']),
|
||||||
|
"end_date": str(row['end_date'])
|
||||||
|
})
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"holders": holders,
|
||||||
|
"total_ratio": sum(holder['hold_ratio'] for holder in holders), # 合计持股比例
|
||||||
|
"report_date": str(latest_end_date) # 报告期
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取股东数据失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
print(f"详细错误: {traceback.format_exc()}")
|
||||||
|
return {"error": f"获取股东数据失败: {str(e)}"}
|
||||||
|
|
||||||
|
def get_value_analysis_data(self, stock_code: str):
|
||||||
|
"""获取价值投资分析所需的关键财务指标"""
|
||||||
|
try:
|
||||||
|
# 处理股票代码格式
|
||||||
|
if stock_code.startswith('6'):
|
||||||
|
ts_code = f"{stock_code}.SH"
|
||||||
|
elif stock_code.startswith(('0', '3')):
|
||||||
|
ts_code = f"{stock_code}.SZ"
|
||||||
|
else:
|
||||||
|
return {"error": "不支持的股票代码"}
|
||||||
|
|
||||||
|
# 获取最新每日指标(估值数据)
|
||||||
|
daily_basic = pro.daily_basic(ts_code=ts_code, fields='pe,pb,ps,dv_ratio,total_mv', limit=1)
|
||||||
|
if daily_basic.empty:
|
||||||
|
return {"error": "无法获取股票估值数据"}
|
||||||
|
|
||||||
|
# 获取最新财务指标
|
||||||
|
fina = pro.fina_indicator(ts_code=ts_code, fields='''roe,grossprofit_margin,netprofit_margin,
|
||||||
|
netprofit_yoy,dt_netprofit_yoy,tr_yoy,or_yoy,assets_turn,inv_turn,ar_turn,current_ratio,
|
||||||
|
quick_ratio,debt_to_assets,ocf_to_or,ocf_yoy,eps,bps,cfps,ocfps,retainedps''', limit=1)
|
||||||
|
if fina.empty:
|
||||||
|
return {"error": "无法获取财务指标数据"}
|
||||||
|
|
||||||
|
# 获取股票名称和当前价格
|
||||||
|
basic_info = pro.daily(ts_code=ts_code, fields='close,trade_date', limit=1)
|
||||||
|
stock_name = pro.stock_basic(ts_code=ts_code, fields='name').iloc[0]['name']
|
||||||
|
|
||||||
|
# 整合数据
|
||||||
|
latest_daily = daily_basic.iloc[0]
|
||||||
|
latest_fina = fina.iloc[0]
|
||||||
|
latest_price = basic_info.iloc[0]
|
||||||
|
|
||||||
|
analysis_data = {
|
||||||
|
"stock_info": {
|
||||||
|
"code": stock_code,
|
||||||
|
"name": stock_name,
|
||||||
|
"current_price": float(latest_price['close']),
|
||||||
|
"trade_date": str(latest_price['trade_date'])
|
||||||
|
},
|
||||||
|
"valuation": {
|
||||||
|
"pe_ratio": float(latest_daily['pe']) if pd.notna(latest_daily['pe']) else None,
|
||||||
|
"pb_ratio": float(latest_daily['pb']) if pd.notna(latest_daily['pb']) else None,
|
||||||
|
"ps_ratio": float(latest_daily['ps']) if pd.notna(latest_daily['ps']) else None,
|
||||||
|
"dividend_yield": float(latest_daily['dv_ratio'])/100 if pd.notna(latest_daily['dv_ratio']) else None,
|
||||||
|
"total_market_value": float(latest_daily['total_mv'])/10000 if pd.notna(latest_daily['total_mv']) else None # 转换为亿元
|
||||||
|
},
|
||||||
|
"profitability": {
|
||||||
|
"roe": float(latest_fina['roe'])/100 if pd.notna(latest_fina['roe']) else None,
|
||||||
|
"gross_margin": float(latest_fina['grossprofit_margin'])/100 if pd.notna(latest_fina['grossprofit_margin']) else None,
|
||||||
|
"net_margin": float(latest_fina['netprofit_margin'])/100 if pd.notna(latest_fina['netprofit_margin']) else None
|
||||||
|
},
|
||||||
|
"growth": {
|
||||||
|
"net_profit_growth": float(latest_fina['netprofit_yoy'])/100 if pd.notna(latest_fina['netprofit_yoy']) else None,
|
||||||
|
"deducted_net_profit_growth": float(latest_fina['dt_netprofit_yoy'])/100 if pd.notna(latest_fina['dt_netprofit_yoy']) else None,
|
||||||
|
"revenue_growth": float(latest_fina['tr_yoy'])/100 if pd.notna(latest_fina['tr_yoy']) else None,
|
||||||
|
"operating_revenue_growth": float(latest_fina['or_yoy'])/100 if pd.notna(latest_fina['or_yoy']) else None
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"asset_turnover": float(latest_fina['assets_turn']) if pd.notna(latest_fina['assets_turn']) else None,
|
||||||
|
"inventory_turnover": float(latest_fina['inv_turn']) if pd.notna(latest_fina['inv_turn']) else None,
|
||||||
|
"receivables_turnover": float(latest_fina['ar_turn']) if pd.notna(latest_fina['ar_turn']) else None
|
||||||
|
},
|
||||||
|
"solvency": {
|
||||||
|
"current_ratio": float(latest_fina['current_ratio']) if pd.notna(latest_fina['current_ratio']) else None,
|
||||||
|
"quick_ratio": float(latest_fina['quick_ratio']) if pd.notna(latest_fina['quick_ratio']) else None,
|
||||||
|
"debt_to_assets": float(latest_fina['debt_to_assets'])/100 if pd.notna(latest_fina['debt_to_assets']) else None
|
||||||
|
},
|
||||||
|
"cash_flow": {
|
||||||
|
"ocf_to_revenue": float(latest_fina['ocf_to_or'])/100 if pd.notna(latest_fina['ocf_to_or']) else None,
|
||||||
|
"ocf_growth": float(latest_fina['ocf_yoy'])/100 if pd.notna(latest_fina['ocf_yoy']) else None
|
||||||
|
},
|
||||||
|
"per_share": {
|
||||||
|
"eps": float(latest_fina['eps']) if pd.notna(latest_fina['eps']) else None,
|
||||||
|
"bps": float(latest_fina['bps']) if pd.notna(latest_fina['bps']) else None,
|
||||||
|
"cfps": float(latest_fina['cfps']) if pd.notna(latest_fina['cfps']) else None,
|
||||||
|
"ocfps": float(latest_fina['ocfps']) if pd.notna(latest_fina['ocfps']) else None,
|
||||||
|
"retained_eps": float(latest_fina['retainedps']) if pd.notna(latest_fina['retainedps']) else None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取价值投资分析数据失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
print(f"详细错误: {traceback.format_exc()}")
|
||||||
|
return {"error": f"获取价值投资分析数据失败: {str(e)}"}
|
||||||
1514
app/templates/index.html
Normal file
1514
app/templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
223
app/templates/market.html
Normal file
223
app/templates/market.html
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>指数行情 - 价值投资盯盘系统</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||||
|
height: 50px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.navbar-brand {
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-right: 50px;
|
||||||
|
}
|
||||||
|
.navbar-nav {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
.nav-link {
|
||||||
|
color: white !important;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 50px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.nav-link.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.iconfont {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.index-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.index-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1400px;
|
||||||
|
}
|
||||||
|
.index-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.index-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #1e3c72;
|
||||||
|
}
|
||||||
|
.index-price {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.index-change {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.index-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.positive-value {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
.negative-value {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand navbar-dark">
|
||||||
|
<div class="container">
|
||||||
|
<span class="navbar-brand">
|
||||||
|
<i class="bi bi-graph-up"></i>
|
||||||
|
价值投资盯盘系统
|
||||||
|
</span>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<a class="nav-link" href="/">监控列表</a>
|
||||||
|
<a class="nav-link active" href="/market">指数行情</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="index-container">
|
||||||
|
<div class="index-grid" id="indexList">
|
||||||
|
<!-- 指数数据将通过JavaScript动态添加 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// 获取指数数据
|
||||||
|
async function refreshIndexData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/index_info');
|
||||||
|
const data = await response.json();
|
||||||
|
updateIndexDisplay(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取指数数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新指数显示
|
||||||
|
function updateIndexDisplay(indexData) {
|
||||||
|
const indexList = document.getElementById('indexList');
|
||||||
|
indexList.innerHTML = '';
|
||||||
|
|
||||||
|
indexData.forEach(index => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'index-card';
|
||||||
|
|
||||||
|
const changeClass = index.change >= 0 ? 'positive-value' : 'negative-value';
|
||||||
|
const changeSign = index.change >= 0 ? '+' : '';
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="index-name">${index.name}</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="index-price">${index.price.toFixed(2)}</div>
|
||||||
|
<div class="index-change ${changeClass}">${changeSign}${index.change.toFixed(2)}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="index-chart" id="chart_${index.code.replace('.', '_')}"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
indexList.appendChild(card);
|
||||||
|
|
||||||
|
// 创建K线图
|
||||||
|
setTimeout(() => {
|
||||||
|
createKlineChart(index.code, index.kline_data);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建K线图
|
||||||
|
function createKlineChart(code, klineData) {
|
||||||
|
const chartDom = document.getElementById(`chart_${code.replace('.', '_')}`);
|
||||||
|
const myChart = echarts.init(chartDom);
|
||||||
|
|
||||||
|
const dates = klineData.map(item => item.date);
|
||||||
|
const data = klineData.map(item => [item.open, item.close, item.low, item.high]);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
animation: false,
|
||||||
|
grid: {
|
||||||
|
left: '8%',
|
||||||
|
right: '8%',
|
||||||
|
top: '10%',
|
||||||
|
bottom: '10%'
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: dates,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#ddd'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
formatter: value => value.substring(4),
|
||||||
|
color: '#666'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
scale: true,
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#eee'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#666'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
type: 'candlestick',
|
||||||
|
data: data,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#ef5350',
|
||||||
|
color0: '#26a69a',
|
||||||
|
borderColor: '#ef5350',
|
||||||
|
borderColor0: '#26a69a'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
myChart.setOption(option);
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
myChart.resize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取数据
|
||||||
|
document.addEventListener('DOMContentLoaded', refreshIndexData);
|
||||||
|
// 每分钟更新一次数据
|
||||||
|
setInterval(refreshIndexData, 60000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
config.json.example
Normal file
4
config.json.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"watchlist": {},
|
||||||
|
"tushare_token": "your_tushare_token_here"
|
||||||
|
}
|
||||||
54
requirements.txt
Normal file
54
requirements.txt
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
aiofiles==23.2.1
|
||||||
|
akshare==1.11.22
|
||||||
|
annotated-types==0.7.0
|
||||||
|
anyio==3.7.1
|
||||||
|
baostock==0.8.9
|
||||||
|
beautifulsoup4==4.12.3
|
||||||
|
bs4==0.0.2
|
||||||
|
certifi==2024.12.14
|
||||||
|
charset-normalizer==3.4.1
|
||||||
|
click==8.1.8
|
||||||
|
decorator==5.1.1
|
||||||
|
efinance==0.4.8
|
||||||
|
et_xmlfile==2.0.0
|
||||||
|
fastapi==0.104.1
|
||||||
|
h11==0.14.0
|
||||||
|
html5lib==1.1
|
||||||
|
idna==3.10
|
||||||
|
Jinja2==3.1.2
|
||||||
|
jsonpath==0.82.2
|
||||||
|
lxml==5.3.0
|
||||||
|
markdown-it-py==3.0.0
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
mdurl==0.1.2
|
||||||
|
multitasking==0.0.11
|
||||||
|
numpy==2.2.1
|
||||||
|
openpyxl==3.1.5
|
||||||
|
pandas==2.2.3
|
||||||
|
py==1.11.0
|
||||||
|
py_mini_racer==0.6.0
|
||||||
|
pydantic==2.10.4
|
||||||
|
pydantic_core==2.27.2
|
||||||
|
Pygments==2.19.1
|
||||||
|
pypinyin==0.53.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
pytz==2024.2
|
||||||
|
requests==2.32.3
|
||||||
|
retry==0.9.2
|
||||||
|
rich==13.9.4
|
||||||
|
simplejson==3.19.3
|
||||||
|
six==1.17.0
|
||||||
|
sniffio==1.3.1
|
||||||
|
soupsieve==2.6
|
||||||
|
starlette==0.27.0
|
||||||
|
tabulate==0.9.0
|
||||||
|
tqdm==4.67.1
|
||||||
|
tushare==1.4.16
|
||||||
|
typing_extensions==4.12.2
|
||||||
|
tzdata==2024.2
|
||||||
|
urllib3==2.3.0
|
||||||
|
uvicorn==0.24.0
|
||||||
|
webencodings==0.5.1
|
||||||
|
websocket-client==1.8.0
|
||||||
|
xlrd==2.0.1
|
||||||
12
run.py
Normal file
12
run.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import uvicorn
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"app:app",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8000, # 修改为8000端口
|
||||||
|
reload=True, # 启用热重载
|
||||||
|
log_level="debug", # 设置日志级别为debug
|
||||||
|
workers=1 # 开发模式使用单个worker
|
||||||
|
)
|
||||||
2283
stock_cache.json
Normal file
2283
stock_cache.json
Normal file
File diff suppressed because it is too large
Load Diff
38
watchlist.json
Normal file
38
watchlist.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"sh600601": {
|
||||||
|
"target_market_value": null,
|
||||||
|
"target_pe": null,
|
||||||
|
"target_pb": null,
|
||||||
|
"target_dividend_yield": null
|
||||||
|
},
|
||||||
|
"sz0024515": {
|
||||||
|
"target_market_value": null,
|
||||||
|
"target_pe": null,
|
||||||
|
"target_pb": null,
|
||||||
|
"target_dividend_yield": null
|
||||||
|
},
|
||||||
|
"sz870199": {
|
||||||
|
"target_market_value": null,
|
||||||
|
"target_pe": null,
|
||||||
|
"target_pb": null,
|
||||||
|
"target_dividend_yield": null
|
||||||
|
},
|
||||||
|
"601318": {
|
||||||
|
"target_market_value": null,
|
||||||
|
"target_pe": null,
|
||||||
|
"target_pb": null,
|
||||||
|
"target_dividend_yield": null
|
||||||
|
},
|
||||||
|
"600601": {
|
||||||
|
"target_market_value": null,
|
||||||
|
"target_pe": null,
|
||||||
|
"target_pb": null,
|
||||||
|
"target_dividend_yield": null
|
||||||
|
},
|
||||||
|
"300059": {
|
||||||
|
"target_market_value": null,
|
||||||
|
"target_pe": null,
|
||||||
|
"target_pb": null,
|
||||||
|
"target_dividend_yield": null
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user