commit;
This commit is contained in:
parent
7f478d91f4
commit
5b9ae03000
43
data.sql
43
data.sql
|
@ -1,43 +0,0 @@
|
||||||
|
|
||||||
CREATE TABLE `gp_day_data` (
|
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
|
||||||
`symbol` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '个股代码',
|
|
||||||
`timestamp` timestamp NULL DEFAULT NULL COMMENT '时间戳',
|
|
||||||
`volume` bigint NULL DEFAULT NULL COMMENT '数量',
|
|
||||||
`open` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '开始价',
|
|
||||||
`high` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '最高价',
|
|
||||||
`low` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '最低价',
|
|
||||||
`close` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '结束价',
|
|
||||||
`chg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '变化数值',
|
|
||||||
`percent` decimal(10, 2) NULL DEFAULT NULL COMMENT '变化百分比',
|
|
||||||
`turnoverrate` decimal(10, 2) NULL DEFAULT NULL COMMENT '换手率',
|
|
||||||
`amount` bigint NULL DEFAULT NULL COMMENT '成交金额',
|
|
||||||
`pb` decimal(10, 2) NULL DEFAULT NULL COMMENT '当前PB',
|
|
||||||
`pe` decimal(10, 2) NULL DEFAULT NULL COMMENT '当前PE',
|
|
||||||
`ps` decimal(10, 2) NULL DEFAULT NULL COMMENT '当前PS',
|
|
||||||
PRIMARY KEY (`id`) USING BTREE,
|
|
||||||
INDEX `idx_symbol`(`symbol` ASC) USING BTREE,
|
|
||||||
INDEX `idx_timestamp`(`timestamp` ASC) USING BTREE
|
|
||||||
) ENGINE = InnoDB AUTO_INCREMENT = 20472590 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
|
|
||||||
|
|
||||||
CREATE TABLE `gp_gnbk` (
|
|
||||||
`id` bigint NULL DEFAULT NULL,
|
|
||||||
`bk_code` bigint NULL DEFAULT NULL,
|
|
||||||
`bk_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
|
||||||
`gp_code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
|
||||||
`gp_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL
|
|
||||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for gp_hybk
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `gp_hybk`;
|
|
||||||
CREATE TABLE `gp_hybk` (
|
|
||||||
`id` bigint NULL DEFAULT NULL,
|
|
||||||
`bk_code` bigint NULL DEFAULT NULL,
|
|
||||||
`bk_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
|
||||||
`gp_code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
|
||||||
`gp_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL
|
|
||||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
|
@ -16,3 +16,4 @@ markdown2>=2.5.3
|
||||||
google-genai
|
google-genai
|
||||||
redis==5.2.1
|
redis==5.2.1
|
||||||
pandas==2.2.3
|
pandas==2.2.3
|
||||||
|
apscheduler==3.11.0
|
|
@ -980,3 +980,5 @@ curl -X POST http://localhost:5000/api/comprehensive_analysis \
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
PE--top bottom
|
||||||
|
PB--top bottom
|
721
src/app.py
721
src/app.py
|
@ -30,6 +30,21 @@ from src.valuation_analysis.industry_analysis import IndustryAnalyzer
|
||||||
# 导入沪深港通监控器
|
# 导入沪深港通监控器
|
||||||
from src.valuation_analysis.hsgt_monitor import HSGTMonitor
|
from src.valuation_analysis.hsgt_monitor import HSGTMonitor
|
||||||
|
|
||||||
|
# 导入融资融券数据采集器
|
||||||
|
from src.valuation_analysis.eastmoney_rzrq_collector import EastmoneyRzrqCollector
|
||||||
|
|
||||||
|
# 导入恐贪指数管理器
|
||||||
|
from src.valuation_analysis.fear_greed_index import FearGreedIndexManager
|
||||||
|
|
||||||
|
# 导入指数分析器
|
||||||
|
from src.valuation_analysis.index_analyzer import IndexAnalyzer
|
||||||
|
|
||||||
|
# 导入股票日线数据采集器
|
||||||
|
from src.scripts.stock_daily_data_collector import collect_stock_daily_data
|
||||||
|
|
||||||
|
from utils.distributed_lock import DistributedLock
|
||||||
|
from valuation_analysis.industry_analysis import redis_client
|
||||||
|
|
||||||
# 设置日志
|
# 设置日志
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
@ -65,6 +80,15 @@ industry_analyzer = IndustryAnalyzer()
|
||||||
# 创建监控器实例
|
# 创建监控器实例
|
||||||
hsgt_monitor = HSGTMonitor()
|
hsgt_monitor = HSGTMonitor()
|
||||||
|
|
||||||
|
# 创建融资融券数据采集器实例
|
||||||
|
em_rzrq_collector = EastmoneyRzrqCollector()
|
||||||
|
|
||||||
|
# 创建恐贪指数管理器实例
|
||||||
|
fear_greed_manager = FearGreedIndexManager()
|
||||||
|
|
||||||
|
# 创建指数分析器实例
|
||||||
|
index_analyzer = IndexAnalyzer()
|
||||||
|
|
||||||
# 获取项目根目录
|
# 获取项目根目录
|
||||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
REPORTS_DIR = os.path.join(ROOT_DIR, 'src', 'reports')
|
REPORTS_DIR = os.path.join(ROOT_DIR, 'src', 'reports')
|
||||||
|
@ -76,6 +100,9 @@ logger.info(f"报告目录路径: {REPORTS_DIR}")
|
||||||
# 存储回测任务状态的字典
|
# 存储回测任务状态的字典
|
||||||
backtest_tasks = {}
|
backtest_tasks = {}
|
||||||
|
|
||||||
|
# 融资融券数据采集任务列表
|
||||||
|
rzrq_tasks = {}
|
||||||
|
|
||||||
def run_backtest_task(task_id, stocks_buy_dates, end_date):
|
def run_backtest_task(task_id, stocks_buy_dates, end_date):
|
||||||
"""
|
"""
|
||||||
在后台运行回测任务
|
在后台运行回测任务
|
||||||
|
@ -156,6 +183,154 @@ def run_backtest_task(task_id, stocks_buy_dates, end_date):
|
||||||
backtest_tasks[task_id]['error'] = str(e)
|
backtest_tasks[task_id]['error'] = str(e)
|
||||||
logger.error(f"回测任务 {task_id} 失败:{str(e)}")
|
logger.error(f"回测任务 {task_id} 失败:{str(e)}")
|
||||||
|
|
||||||
|
def initialize_rzrq_collector_schedule():
|
||||||
|
"""初始化融资融券数据采集定时任务"""
|
||||||
|
# 创建分布式锁
|
||||||
|
rzrq_lock = DistributedLock(redis_client, "em_rzrq_collector", expire_time=3600) # 1小时过期
|
||||||
|
|
||||||
|
# 尝试获取锁
|
||||||
|
if not rzrq_lock.acquire():
|
||||||
|
logger.info("其他服务器正在运行融资融券数据采集任务,本服务器跳过")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
# 创建定时任务调度器
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
|
||||||
|
# 添加每天下午5点执行的任务
|
||||||
|
scheduler.add_job(
|
||||||
|
func=run_rzrq_initial_collection,
|
||||||
|
trigger=CronTrigger(hour=18, minute=0),
|
||||||
|
id='rzrq_daily_update',
|
||||||
|
name='每日更新融资融券数据',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 启动调度器
|
||||||
|
scheduler.start()
|
||||||
|
logger.info("融资融券数据采集定时任务已初始化,将在每天18:00执行")
|
||||||
|
return scheduler
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"初始化融资融券数据采集定时任务失败: {str(e)}")
|
||||||
|
rzrq_lock.release()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def initialize_stock_daily_collector_schedule():
|
||||||
|
"""初始化股票日线数据采集定时任务"""
|
||||||
|
# 创建分布式锁
|
||||||
|
stock_daily_lock = DistributedLock(redis_client, "stock_daily_collector", expire_time=3600) # 1小时过期
|
||||||
|
|
||||||
|
# 尝试获取锁
|
||||||
|
if not stock_daily_lock.acquire():
|
||||||
|
logger.info("其他服务器正在运行股票日线数据采集任务,本服务器跳过")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
# 创建定时任务调度器
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
|
||||||
|
# 添加每天下午5点执行的任务
|
||||||
|
scheduler.add_job(
|
||||||
|
func=run_stock_daily_collection,
|
||||||
|
trigger=CronTrigger(hour=15, minute=40),
|
||||||
|
id='stock_daily_update',
|
||||||
|
name='每日更新股票日线数据',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 启动调度器
|
||||||
|
scheduler.start()
|
||||||
|
logger.info("股票日线数据采集定时任务已初始化,将在每天15:40执行")
|
||||||
|
return scheduler
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"初始化股票日线数据采集定时任务失败: {str(e)}")
|
||||||
|
stock_daily_lock.release()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run_stock_daily_collection():
|
||||||
|
"""执行股票日线数据采集任务"""
|
||||||
|
try:
|
||||||
|
logger.info("开始执行股票日线数据采集")
|
||||||
|
|
||||||
|
# 获取当天日期
|
||||||
|
today = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# 定义数据库连接地址
|
||||||
|
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
|
||||||
|
|
||||||
|
# 在新线程中执行采集任务,避免阻塞主线程
|
||||||
|
def collection_task():
|
||||||
|
try:
|
||||||
|
# 执行采集
|
||||||
|
collect_stock_daily_data(db_url, today)
|
||||||
|
logger.info(f"股票日线数据采集完成,日期: {today}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行股票日线数据采集任务失败: {str(e)}")
|
||||||
|
|
||||||
|
# 创建并启动线程
|
||||||
|
thread = Thread(target=collection_task)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动股票日线数据采集任务失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_rzrq_initial_collection():
|
||||||
|
"""执行融资融券数据初始全量采集"""
|
||||||
|
try:
|
||||||
|
logger.info("开始执行融资融券数据初始全量采集")
|
||||||
|
|
||||||
|
# 生成任务ID
|
||||||
|
task_id = f"rzrq-{uuid.uuid4().hex[:16]}"
|
||||||
|
|
||||||
|
# 记录任务信息
|
||||||
|
rzrq_tasks[task_id] = {
|
||||||
|
'status': 'running',
|
||||||
|
'created_at': datetime.now().isoformat(),
|
||||||
|
'type': 'initial_collection',
|
||||||
|
'message': '开始执行融资融券数据初始全量采集'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 在新线程中执行采集任务
|
||||||
|
def collection_task():
|
||||||
|
try:
|
||||||
|
# 执行采集
|
||||||
|
result = em_rzrq_collector.initial_data_collection()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
rzrq_tasks[task_id]['status'] = 'completed'
|
||||||
|
rzrq_tasks[task_id]['message'] = '融资融券数据初始全量采集完成'
|
||||||
|
logger.info(f"融资融券数据初始全量采集任务 {task_id} 完成")
|
||||||
|
else:
|
||||||
|
rzrq_tasks[task_id]['status'] = 'failed'
|
||||||
|
rzrq_tasks[task_id]['message'] = '融资融券数据初始全量采集失败'
|
||||||
|
logger.error(f"融资融券数据初始全量采集任务 {task_id} 失败")
|
||||||
|
except Exception as e:
|
||||||
|
rzrq_tasks[task_id]['status'] = 'failed'
|
||||||
|
rzrq_tasks[task_id]['message'] = f'执行失败: {str(e)}'
|
||||||
|
logger.error(f"执行融资融券数据初始全量采集线程中出错: {str(e)}")
|
||||||
|
|
||||||
|
# 创建并启动线程
|
||||||
|
thread = Thread(target=collection_task)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return task_id
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动融资融券数据初始全量采集任务失败: {str(e)}")
|
||||||
|
if 'task_id' in locals():
|
||||||
|
rzrq_tasks[task_id]['status'] = 'failed'
|
||||||
|
rzrq_tasks[task_id]['message'] = f'启动失败: {str(e)}'
|
||||||
|
return None
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""渲染主页"""
|
"""渲染主页"""
|
||||||
|
@ -1558,6 +1733,45 @@ def get_industry_list():
|
||||||
"message": f"获取行业列表失败: {str(e)}"
|
"message": f"获取行业列表失败: {str(e)}"
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/concept/list', methods=['GET'])
|
||||||
|
def get_concept_list():
|
||||||
|
"""
|
||||||
|
获取概念板块列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": [
|
||||||
|
{"code": "200001", "name": "人工智能"},
|
||||||
|
{"code": "200002", "name": "大数据"},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用IndustryAnalyzer获取概念板块列表
|
||||||
|
concepts = industry_analyzer.get_concept_list()
|
||||||
|
|
||||||
|
if not concepts:
|
||||||
|
logger.warning("未找到概念板块数据")
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": "未找到概念板块数据"
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"count": len(concepts),
|
||||||
|
"data": concepts
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取概念板块列表失败: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"获取概念板块列表失败: {str(e)}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/industry/analysis', methods=['GET'])
|
@app.route('/api/industry/analysis', methods=['GET'])
|
||||||
def industry_analysis():
|
def industry_analysis():
|
||||||
"""
|
"""
|
||||||
|
@ -1875,6 +2089,67 @@ def get_southbound_data():
|
||||||
"message": f"服务器错误: {str(e)}"
|
"message": f"服务器错误: {str(e)}"
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/rzrq/chart_data', methods=['GET'])
|
||||||
|
def get_rzrq_chart_data():
|
||||||
|
"""获取融资融券数据用于图表展示
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- days: 可选,获取最近多少天的数据(默认30天)
|
||||||
|
|
||||||
|
返回内容:
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"success": true,
|
||||||
|
"dates": ["2023-01-01", "2023-01-02", ...],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "融资融券余额合计",
|
||||||
|
"data": [1234.56, 1235.67, ...],
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
// 其他系列数据...
|
||||||
|
],
|
||||||
|
"last_update": "2023-01-15 12:34:56"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取天数参数
|
||||||
|
days = request.args.get('days', type=int, default=30)
|
||||||
|
|
||||||
|
# 限制天数范围
|
||||||
|
if days <= 0:
|
||||||
|
days = 30
|
||||||
|
elif days > 365:
|
||||||
|
days = 365
|
||||||
|
|
||||||
|
# 调用数据获取方法
|
||||||
|
result = em_rzrq_collector.get_chart_data(limit_days=days)
|
||||||
|
|
||||||
|
if result.get('success'):
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"data": result
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": result.get('message', '获取融资融券数据失败')
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"参数格式错误: {str(e)}"
|
||||||
|
}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取融资融券图表数据异常: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"服务器错误: {str(e)}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/stock/tracks', methods=['GET'])
|
@app.route('/api/stock/tracks', methods=['GET'])
|
||||||
def get_stock_tracks():
|
def get_stock_tracks():
|
||||||
"""根据股票代码获取相关赛道信息
|
"""根据股票代码获取相关赛道信息
|
||||||
|
@ -1939,5 +2214,451 @@ def get_stock_tracks():
|
||||||
"message": f"服务器错误: {str(e)}"
|
"message": f"服务器错误: {str(e)}"
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/stock/price_range', methods=['GET'])
|
||||||
|
def get_stock_price_range():
|
||||||
|
"""根据股票估值分位计算理论价格区间
|
||||||
|
|
||||||
|
根据当前PE和PB的四分位数据,反向计算出对应的理论股价区间
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- stock_code: 必须,股票代码
|
||||||
|
- start_date: 可选,开始日期,默认为一年前
|
||||||
|
|
||||||
|
返回内容:
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"stock_code": "600000",
|
||||||
|
"stock_name": "浦发银行",
|
||||||
|
"current_price": 10.5,
|
||||||
|
"current_date": "2023-12-01",
|
||||||
|
"pe": {
|
||||||
|
"current": 5.2,
|
||||||
|
"q1": 4.8,
|
||||||
|
"q3": 6.5,
|
||||||
|
"q1_price": 9.7, // 对应PE为Q1时的理论股价
|
||||||
|
"q3_price": 13.1 // 对应PE为Q3时的理论股价
|
||||||
|
},
|
||||||
|
"pb": {
|
||||||
|
"current": 0.65,
|
||||||
|
"q1": 0.6,
|
||||||
|
"q3": 0.8,
|
||||||
|
"q1_price": 9.7, // 对应PB为Q1时的理论股价
|
||||||
|
"q3_price": 12.9 // 对应PB为Q3时的理论股价
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取股票代码参数
|
||||||
|
stock_code = request.args.get('stock_code')
|
||||||
|
|
||||||
|
# 验证参数
|
||||||
|
if not stock_code:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": "缺少必要参数: stock_code"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 计算一年前的日期作为默认起始日期
|
||||||
|
default_start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
|
||||||
|
start_date = request.args.get('start_date', default_start_date)
|
||||||
|
|
||||||
|
# 通过复用现有API的逻辑获取PE和PB数据
|
||||||
|
# 首先获取PE数据
|
||||||
|
pe_data = valuation_analyzer.get_historical_data(stock_code, start_date)
|
||||||
|
if pe_data.empty:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"未找到股票 {stock_code} 的历史数据"
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# 计算PE分位数
|
||||||
|
pe_percentiles = valuation_analyzer.calculate_percentiles(pe_data, 'pe')
|
||||||
|
if not pe_percentiles:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"无法计算股票 {stock_code} 的PE分位数"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# 计算PB分位数
|
||||||
|
pb_percentiles = valuation_analyzer.calculate_percentiles(pe_data, 'pb')
|
||||||
|
if not pb_percentiles:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"无法计算股票 {stock_code} 的PB分位数"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# 获取当前股价
|
||||||
|
current_price = None
|
||||||
|
current_date = None
|
||||||
|
if not pe_data.empty:
|
||||||
|
current_price = pe_data.iloc[-1].get('close')
|
||||||
|
current_date = pe_data.iloc[-1].get('timestamp').strftime('%Y-%m-%d') if 'timestamp' in pe_data.columns else None
|
||||||
|
|
||||||
|
if current_price is None:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"无法获取股票 {stock_code} 的当前股价"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# 获取当前PE和PB
|
||||||
|
current_pe = pe_percentiles.get('current')
|
||||||
|
current_pb = pb_percentiles.get('current')
|
||||||
|
|
||||||
|
# 获取PE的Q1和Q3
|
||||||
|
pe_q1 = pe_percentiles.get('q1')
|
||||||
|
pe_q3 = pe_percentiles.get('q3')
|
||||||
|
|
||||||
|
# 获取PB的Q1和Q3
|
||||||
|
pb_q1 = pb_percentiles.get('q1')
|
||||||
|
pb_q3 = pb_percentiles.get('q3')
|
||||||
|
|
||||||
|
# 反向计算估值分位对应的股价
|
||||||
|
# 如果当前PE为X,股价为Y,则PE为Z时的理论股价 = Y * (X / Z)
|
||||||
|
|
||||||
|
# 计算PE对应的理论股价
|
||||||
|
pe_q1_price = None
|
||||||
|
pe_q3_price = None
|
||||||
|
if current_pe and current_pe > 0 and pe_q1 and pe_q3:
|
||||||
|
pe_q1_price = current_price * (pe_q1 / current_pe)
|
||||||
|
pe_q3_price = current_price * (pe_q3 / current_pe)
|
||||||
|
|
||||||
|
# 计算PB对应的理论股价
|
||||||
|
pb_q1_price = None
|
||||||
|
pb_q3_price = None
|
||||||
|
if current_pb and current_pb > 0 and pb_q1 and pb_q3:
|
||||||
|
pb_q1_price = current_price * (pb_q1 / current_pb)
|
||||||
|
pb_q3_price = current_price * (pb_q3 / current_pb)
|
||||||
|
|
||||||
|
# 获取股票名称
|
||||||
|
stock_name = valuation_analyzer.get_stock_name(stock_code)
|
||||||
|
|
||||||
|
# 构建响应
|
||||||
|
response = {
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"stock_code": stock_code,
|
||||||
|
"stock_name": stock_name,
|
||||||
|
"current_price": current_price,
|
||||||
|
"current_date": current_date,
|
||||||
|
"pe": {
|
||||||
|
"current": current_pe,
|
||||||
|
"q1": pe_q1,
|
||||||
|
"q3": pe_q3,
|
||||||
|
"q1_price": round(pe_q1_price, 2) if pe_q1_price is not None else None,
|
||||||
|
"q3_price": round(pe_q3_price, 2) if pe_q3_price is not None else None
|
||||||
|
},
|
||||||
|
"pb": {
|
||||||
|
"current": current_pb,
|
||||||
|
"q1": pb_q1,
|
||||||
|
"q3": pb_q3,
|
||||||
|
"q1_price": round(pb_q1_price, 2) if pb_q1_price is not None else None,
|
||||||
|
"q3_price": round(pb_q3_price, 2) if pb_q3_price is not None else None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"计算股票价格区间异常: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"服务器错误: {str(e)}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/fear_greed/data', methods=['GET'])
|
||||||
|
def get_fear_greed_data():
|
||||||
|
"""获取恐贪指数数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- start_date: 可选,开始日期(YYYY-MM-DD格式)
|
||||||
|
- end_date: 可选,结束日期(YYYY-MM-DD格式)
|
||||||
|
- limit: 可选,限制返回的记录数量,默认为730(约两年的交易日数量)
|
||||||
|
|
||||||
|
返回内容:
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"dates": ["2023-01-01", "2023-01-02", ...],
|
||||||
|
"values": [45.67, 50.12, ...],
|
||||||
|
"latest": {
|
||||||
|
"id": 123,
|
||||||
|
"index_value": 50.12,
|
||||||
|
"trading_date": "2023-01-02",
|
||||||
|
"update_time": "2023-01-02 15:30:00"
|
||||||
|
},
|
||||||
|
"latest_status": "中性",
|
||||||
|
"update_time": "2023-01-02 16:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取参数
|
||||||
|
start_date = request.args.get('start_date')
|
||||||
|
end_date = request.args.get('end_date')
|
||||||
|
limit = request.args.get('limit', type=int, default=730)
|
||||||
|
|
||||||
|
# 调用数据获取方法
|
||||||
|
result = fear_greed_manager.get_index_data(start_date, end_date, limit)
|
||||||
|
|
||||||
|
if result.get('success'):
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"data": result
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": result.get('message', '获取恐贪指数数据失败')
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"参数格式错误: {str(e)}"
|
||||||
|
}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取恐贪指数数据异常: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"服务器错误: {str(e)}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/fear_greed/add', methods=['POST'])
|
||||||
|
def add_fear_greed_data():
|
||||||
|
"""添加恐贪指数数据
|
||||||
|
|
||||||
|
请求体格式:
|
||||||
|
{
|
||||||
|
"index_value": 45.67, // 恐贪指数值,0-100之间的数值
|
||||||
|
"trading_date": "2023-01-01" // 交易日期,YYYY-MM-DD格式
|
||||||
|
}
|
||||||
|
|
||||||
|
返回内容:
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "数据添加成功"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 从请求体获取参数
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": "请求体为空"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
index_value = data.get('index_value')
|
||||||
|
trading_date = data.get('trading_date')
|
||||||
|
|
||||||
|
# 验证参数
|
||||||
|
if index_value is None:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": "缺少必要参数: index_value"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
if trading_date is None:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": "缺少必要参数: trading_date"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 尝试转换为浮点数
|
||||||
|
try:
|
||||||
|
index_value = float(index_value)
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": "index_value必须是数值"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 调用添加方法
|
||||||
|
result = fear_greed_manager.add_index_data(index_value, trading_date)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"message": "恐贪指数数据添加成功"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": "恐贪指数数据添加失败"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"添加恐贪指数数据异常: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"服务器错误: {str(e)}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# 获取可用指数列表
|
||||||
|
@app.route('/api/indices/list', methods=['GET'])
|
||||||
|
def get_indices_list():
|
||||||
|
"""
|
||||||
|
获取可用指数列表
|
||||||
|
|
||||||
|
返回所有可用于叠加显示的指数列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
indices = index_analyzer.get_indices_list()
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"data": indices
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取指数列表失败: {str(e)}")
|
||||||
|
return jsonify({"status": "error", "message": str(e)})
|
||||||
|
|
||||||
|
# 获取指数历史数据
|
||||||
|
@app.route('/api/indices/data', methods=['GET'])
|
||||||
|
def get_index_data():
|
||||||
|
"""
|
||||||
|
获取指数历史数据
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- code: 指数代码
|
||||||
|
- start_date: 开始日期 (可选,默认为1年前)
|
||||||
|
- end_date: 结束日期 (可选,默认为今天)
|
||||||
|
|
||||||
|
返回指数历史收盘价数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
index_code = request.args.get('code')
|
||||||
|
start_date = request.args.get('start_date')
|
||||||
|
end_date = request.args.get('end_date')
|
||||||
|
|
||||||
|
if not index_code:
|
||||||
|
return jsonify({"status": "error", "message": "缺少指数代码参数"})
|
||||||
|
|
||||||
|
index_data = index_analyzer.get_index_data(index_code, start_date, end_date)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"data": index_data
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取指数数据失败: {str(e)}")
|
||||||
|
return jsonify({"status": "error", "message": str(e)})
|
||||||
|
|
||||||
|
def initialize_industry_crowding_schedule():
|
||||||
|
"""初始化行业拥挤度指标预计算定时任务"""
|
||||||
|
# 创建分布式锁
|
||||||
|
industry_crowding_lock = DistributedLock(redis_client, "industry_crowding_calculator", expire_time=3600) # 1小时过期
|
||||||
|
|
||||||
|
# 尝试获取锁
|
||||||
|
if not industry_crowding_lock.acquire():
|
||||||
|
logger.info("其他服务器正在运行行业拥挤度指标预计算任务,本服务器跳过")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
# 创建定时任务调度器
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
|
||||||
|
# 添加每天晚上10点执行的任务
|
||||||
|
scheduler.add_job(
|
||||||
|
func=precalculate_industry_crowding,
|
||||||
|
trigger=CronTrigger(hour=22, minute=0),
|
||||||
|
id='industry_crowding_precalc',
|
||||||
|
name='预计算行业拥挤度指标',
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 启动调度器
|
||||||
|
scheduler.start()
|
||||||
|
logger.info("行业拥挤度指标预计算定时任务已初始化,将在每天22:00执行")
|
||||||
|
return scheduler
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"初始化行业拥挤度指标预计算定时任务失败: {str(e)}")
|
||||||
|
industry_crowding_lock.release()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def precalculate_industry_crowding():
|
||||||
|
"""预计算所有行业的拥挤度指标"""
|
||||||
|
try:
|
||||||
|
logger.info("开始预计算所有行业的拥挤度指标")
|
||||||
|
|
||||||
|
# 获取所有行业列表
|
||||||
|
industries = industry_analyzer.get_industry_list()
|
||||||
|
if not industries:
|
||||||
|
logger.error("获取行业列表失败")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 记录成功和失败的数量
|
||||||
|
success_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
|
||||||
|
# 遍历所有行业
|
||||||
|
for industry in industries:
|
||||||
|
try:
|
||||||
|
industry_name = industry['name']
|
||||||
|
logger.info(f"正在计算行业 {industry_name} 的拥挤度指标")
|
||||||
|
|
||||||
|
# 调用拥挤度计算方法
|
||||||
|
df = industry_analyzer.get_industry_crowding_index(industry_name)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
success_count += 1
|
||||||
|
logger.info(f"成功计算行业 {industry_name} 的拥挤度指标")
|
||||||
|
else:
|
||||||
|
fail_count += 1
|
||||||
|
logger.warning(f"计算行业 {industry_name} 的拥挤度指标失败")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
fail_count += 1
|
||||||
|
logger.error(f"计算行业 {industry_name} 的拥挤度指标时出错: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"行业拥挤度指标预计算完成,成功: {success_count},失败: {fail_count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"预计算行业拥挤度指标失败: {str(e)}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
"""
|
||||||
|
# 手动释放锁的方法(需要时取消注释)
|
||||||
|
# 创建锁实例
|
||||||
|
rzrq_lock = DistributedLock(redis_client, "em_rzrq_collector")
|
||||||
|
stock_daily_lock = DistributedLock(redis_client, "stock_daily_collector")
|
||||||
|
industry_crowding_lock = DistributedLock(redis_client, "industry_crowding_calculator")
|
||||||
|
|
||||||
|
# 强制释放锁
|
||||||
|
print("开始释放锁...")
|
||||||
|
|
||||||
|
if rzrq_lock.release():
|
||||||
|
print("成功释放融资融券采集器锁")
|
||||||
|
else:
|
||||||
|
print("融资融券采集器锁释放失败或不存在")
|
||||||
|
|
||||||
|
if stock_daily_lock.release():
|
||||||
|
print("成功释放股票日线采集器锁")
|
||||||
|
else:
|
||||||
|
print("股票日线采集器锁释放失败或不存在")
|
||||||
|
if industry_crowding_lock.release():
|
||||||
|
print("成功释放股票日线采集器锁")
|
||||||
|
else:
|
||||||
|
print("股票日线采集器锁释放失败或不存在")
|
||||||
|
|
||||||
|
print("锁释放操作完成")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 初始化融资融券数据采集定时任务
|
||||||
|
rzrq_scheduler = initialize_rzrq_collector_schedule()
|
||||||
|
|
||||||
|
# 初始化股票日线数据采集定时任务
|
||||||
|
stock_daily_scheduler = initialize_stock_daily_collector_schedule()
|
||||||
|
|
||||||
|
# 初始化行业拥挤度指标预计算定时任务
|
||||||
|
industry_crowding_scheduler = initialize_industry_crowding_schedule()
|
||||||
|
|
||||||
|
# 启动Web服务器
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
|
@ -11,7 +11,7 @@ XUEQIU_HEADERS = {
|
||||||
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
'Client-Version': 'v2.44.75',
|
'Client-Version': 'v2.44.75',
|
||||||
'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; xq_a_token=90d76a1c24a9d8fd1b868cd7b94fabcdd6cb2f0a; xqat=90d76a1c24a9d8fd1b868cd7b94fabcdd6cb2f0a; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzQ4NTI1NTA4LCJjdG0iOjE3NDU5MzM1MDg4NDcsImNpZCI6ImQ5ZDBuNEFadXAifQ.Xj00ujbYNYb3jt0wev1VZSj37wy3oRdTXohaOXp0xGoV6xOS055QcxaeXzbE6yaKQDgwUC4NVCEQLfJ49LvxWDSvWGEI7y2j-_ZzH-ZoHc6-RZ7pQdLLlTeRSM17Sg1JZZWG4xwk4yb_aHoWyUznjODTOgyg8EOnhDPO6-bI8SrXXXV8a-TE0ZpDw1EIimKYzhCQR0qwEnm2swEoN3YRfyiBvuMg5Cr2zqgnrKQAafquUZmwFvudIVlYG1HppoMnrbzXhQ4II0tP8duvcT-mzabQE_OaY0RM5u9mwthMfm5KPThEVb_o74s_SweMv6vHZDRMaxxzrnlM4MgW-4mmpg; xq_r_token=6a95ad5270dea5256d4b5d14683bf40cdabce730; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746410725; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1746517072; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0QHhqDyGmZATmkDq3e4pEBDDl=BrYDSxD6FDK4GTh86DOICelaOowCWKGLkUpeBUlCR5QW/+Dp0KY0zzSQW0P40aDmKDUoqqhW7YDeWaDCeDQxirDD4DAWPDFxibDiWIo4Ddb+ovz3owpDGrDlKDRx07pYoDbxDaDGpmYCDxbS7eDD5DnESXI4DWDWapeDDzelQx+xoxDm+mk4YLpB80RjgDqYoD9h4DsZigl/LgATiAkS=BWvewff3DvxDk2IEGU4TpKavbec4xqiibt7Y34qe2qF+QKGxAGUmrKiiYiiqP+xmhx84qmx4RxCIT5MqbF7YQFYRxY=7K5iK4rZ0y/mWV/HYerYTBqiAbYEk4hNDRu44bQmtBhN4+QlbqY3PA0PogkWgieD; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0QHhqDyGmZATmkDq3e4pEYDA4gYHYeqRD7Pzg+ReDsXYe6pj6SmpZ2qUqQe6DhjRtXa2S6bph7ZGARuppraeTqypju30Gj37fAmhhj8qrSzx6KdQfXAG4Zj3f5WLPMjTIV77RYy+TnziIlSLPEBg3M3ZuL41LKWTf6lS330QyxSLXCOYnxlCGLl46fKbFElPrcG4=C=IQgQ9tGaCLfmgxZQBQtoiIQprYcbYfuRcCYM1y5OH37aMWU4=yQYv/LnWnGq5OSclDIyYpvCnDYqv9aUBn4=mQR0pGcsjuHQvLm9F7iPmPHYH+CcLjIjGBntKepw870/+FKq52z9YXHYaq4fbH0v2GHseRe=WHIgD9HY=FQrnctq/GFA3EhBKctmx4wvim9+bWX4UI+2FP+b8F9P0lS7rWz3PU9m4NmqwK0Wux6+xjn4qPtcYUD8OKpAYFK42qAid5Dt9RqiiqEiaeQhEo+aQwP2BYIpfihOiY3bre4t9rNnxro0q8GI==I2hDD',
|
'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746410725; xq_a_token=660fb18cf1d15162da76deedc46b649370124dca; xqat=660fb18cf1d15162da76deedc46b649370124dca; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzQ5ODYxNjY5LCJjdG0iOjE3NDcyNjk2Njk0NDgsImNpZCI6ImQ5ZDBuNEFadXAifQ.jc_E9qvguLwBDASn1Z-KjGtU89pNJRwJq_hIaiR3r2re7-_xiXH8qhuhC3Se8rlfKGZ8sHsb3rSND_vnF7yMp90QQHdK_brSmlgd6_ltHmJfWSFNJvMk7F3s0yPjcpeMqeUTPFnZwKmoWwZVKEwdVBN8f25z6e9M2JjtSTZ2huADH_FdEn1rb9IU-H35z_MLWW1M7vB5xc2rh57yFIBnQoxu9OLfeETpeIpASP1UBeZXoQZ_v1gIWiFYItwuudIz0tPYzB-o2duRe31G0S_hNvEGl3HH4M5FjTyaPAq2PRuiZCyRF-25gHXBZnLcxyavZ1VAURfHng_377_IJNSXsw; xq_r_token=8a5dec9c93caf88d0e1f98f1d23ea1bb60eb6225; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1747356850; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40QuHhqDyGGdVmpmghQehYtDDsqze4GzDiLPGhDBWAFdYCdqt4NKWooqCWKCwdUme9Ill25QAClcymm=0Iil4OAe8oGLDY=DCTKK420iDYAEDBYD74G+DDeDiO3Dj4GmDGY=aeDFIQutVCRKdxDwDB=DmqG23ObDm4DfDDLorBD4Il2YDDtDAkaGNPDADA3doDDlYD84edb4DYpogQ0FdgahphuXIeDMixGXzAlzx9CnoiWtV/LfNf2aHPGuDG=OcC0Hh2bmRT3f8hGxYBY5QeOhx+BxorKq0DW7HRYqexx=CD=WKK7oQ7YBGxPG4KiKy7hAQd5dpOodYYrcqsMkbZMshieygdyhxogYO2deGd46DAQ5MA5VBxiT5/h4WB++l=Eet4D; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40QuHhqDyGGdVmpmghQehY4Dfie4pCoTp35CT5NsKziGGtvkoYD',
|
||||||
'Referer': 'https://weibo.com/u/7735765253',
|
'Referer': 'https://weibo.com/u/7735765253',
|
||||||
'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
||||||
'Sec-Ch-Ua-Mobile': '?0',
|
'Sec-Ch-Ua-Mobile': '?0',
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
# coding:utf-8
|
# coding:utf-8
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -6,9 +5,8 @@ import pandas as pd
|
||||||
from sqlalchemy import create_engine, text
|
from sqlalchemy import create_engine, text
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from config import XUEQIU_HEADERS
|
from src.scripts.config import XUEQIU_HEADERS
|
||||||
import gc
|
import gc
|
||||||
import time
|
|
||||||
|
|
||||||
class StockDailyDataCollector:
|
class StockDailyDataCollector:
|
||||||
"""股票日线数据采集器类"""
|
"""股票日线数据采集器类"""
|
||||||
|
@ -23,9 +21,20 @@ class StockDailyDataCollector:
|
||||||
self.headers = XUEQIU_HEADERS
|
self.headers = XUEQIU_HEADERS
|
||||||
|
|
||||||
def fetch_all_stock_codes(self):
|
def fetch_all_stock_codes(self):
|
||||||
query = "SELECT gp_code FROM gp_code_all"
|
# 从gp_code_all获取股票代码
|
||||||
df = pd.read_sql(query, self.engine)
|
query_all = "SELECT gp_code FROM gp_code_all"
|
||||||
return df['gp_code'].tolist()
|
df_all = pd.read_sql(query_all, self.engine)
|
||||||
|
codes_all = df_all['gp_code'].tolist()
|
||||||
|
|
||||||
|
# 从gp_code_zs获取股票代码
|
||||||
|
query_zs = "SELECT gp_code FROM gp_code_zs"
|
||||||
|
df_zs = pd.read_sql(query_zs, self.engine)
|
||||||
|
codes_zs = df_zs['gp_code'].tolist()
|
||||||
|
|
||||||
|
# 合并去重
|
||||||
|
all_codes = list(set(codes_all + codes_zs))
|
||||||
|
print(f"获取到股票代码: {len(codes_all)}个来自gp_code_all, {len(codes_zs)}个来自gp_code_zs, 去重后共{len(all_codes)}个")
|
||||||
|
return all_codes
|
||||||
|
|
||||||
def fetch_daily_stock_data(self, symbol, begin):
|
def fetch_daily_stock_data(self, symbol, begin):
|
||||||
url = f"https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol={symbol}&begin={begin}&period=day&type=before&count=-1&indicator=kline,pe,pb,ps,pcf,market_capital,agt,ggt,balance"
|
url = f"https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol={symbol}&begin={begin}&period=day&type=before&count=-1&indicator=kline,pe,pb,ps,pcf,market_capital,agt,ggt,balance"
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,67 @@
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.money-inflow {
|
||||||
|
color: #d9534f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.money-outflow {
|
||||||
|
color: #5cb85c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.chart-container {
|
||||||
|
height: 350px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.stat-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.refresh-btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.update-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.flow-direction {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: -5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.summary-text {
|
||||||
|
font-size: 1em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.percentage-value {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em; /* 字体放大 */
|
||||||
|
padding: 0 3px; /* 微调间距 */
|
||||||
|
}
|
||||||
|
.percentage-value.positive {
|
||||||
|
color: #d9534f; /* 红色 */
|
||||||
|
}
|
||||||
|
.percentage-value.negative {
|
||||||
|
color: #5cb85c; /* 绿色 */
|
||||||
|
}
|
||||||
|
.percentage-value.neutral {
|
||||||
|
color: #337ab7; /* 蓝色 (中性色) */
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -6,12 +6,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// 初始化图表
|
// 初始化图表
|
||||||
let northChart = null;
|
let northChart = null;
|
||||||
let southChart = null;
|
let southChart = null;
|
||||||
|
let rzrqChart = null; // 融资融券图表实例
|
||||||
|
|
||||||
|
// 当前显示的融资融券数据系列
|
||||||
|
let currentMetric = 'total_rzrq_balance';
|
||||||
|
// 融资融券数据
|
||||||
|
let rzrqData = null;
|
||||||
|
|
||||||
|
// 融资融券图表相关功能
|
||||||
|
let rzrqIndexSelector = null;
|
||||||
|
let rzrqChartData = null; // 用于存储融资融券图表的原始数据
|
||||||
|
|
||||||
// 初始化图表函数,确保DOM元素存在
|
// 初始化图表函数,确保DOM元素存在
|
||||||
function initCharts() {
|
function initCharts() {
|
||||||
try {
|
try {
|
||||||
const northChartDom = document.getElementById('northChart');
|
const northChartDom = document.getElementById('northChart');
|
||||||
const southChartDom = document.getElementById('southChart');
|
const southChartDom = document.getElementById('southChart');
|
||||||
|
const rzrqChartDom = document.getElementById('rzrqChart');
|
||||||
|
|
||||||
if (northChartDom && !northChart) {
|
if (northChartDom && !northChart) {
|
||||||
try {
|
try {
|
||||||
|
@ -31,7 +42,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return northChart && southChart;
|
if (rzrqChartDom && !rzrqChart) {
|
||||||
|
try {
|
||||||
|
rzrqChart = echarts.init(rzrqChartDom);
|
||||||
|
console.log('融资融券图表初始化成功');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('融资融券图表初始化失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return northChart && southChart && rzrqChart;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('图表初始化过程中发生错误:', e);
|
console.error('图表初始化过程中发生错误:', e);
|
||||||
return false;
|
return false;
|
||||||
|
@ -48,10 +68,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// 开始加载数据
|
// 开始加载数据
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
// 设置自动刷新 (每分钟刷新一次)
|
// 加载融资融券数据
|
||||||
setInterval(loadData, 60000);
|
initRzrqChart();
|
||||||
|
|
||||||
|
// 检查是否在交易时段,只有在交易时段才设置自动刷新
|
||||||
|
if (isWithinTradingHours()) {
|
||||||
|
console.log('当前处于交易时段,启用自动刷新');
|
||||||
|
// 设置自动刷新 (每分钟刷新一次)
|
||||||
|
window.refreshInterval = setInterval(loadData, 60000);
|
||||||
|
} else {
|
||||||
|
console.log('当前不在交易时段,不启用自动刷新');
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前时间是否在交易时段内 (9:20-16:00)
|
||||||
|
*/
|
||||||
|
function isWithinTradingHours() {
|
||||||
|
const now = new Date();
|
||||||
|
const hours = now.getHours();
|
||||||
|
const minutes = now.getMinutes();
|
||||||
|
const dayOfWeek = now.getDay(); // 0是周日,6是周六
|
||||||
|
|
||||||
|
// 如果是周末,不在交易时段
|
||||||
|
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前时间的分钟数
|
||||||
|
const totalMinutes = hours * 60 + minutes;
|
||||||
|
|
||||||
|
// 交易时段开始时间:9:20 (9*60 + 20 = 560分钟)
|
||||||
|
const tradingStartMinutes = 9 * 60 + 20;
|
||||||
|
|
||||||
|
// 交易时段结束时间:16:00 (16*60 = 960分钟)
|
||||||
|
const tradingEndMinutes = 16 * 60;
|
||||||
|
|
||||||
|
// 判断当前时间是否在交易时段内
|
||||||
|
return totalMinutes >= tradingStartMinutes && totalMinutes <= tradingEndMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
// 设置图表自适应
|
// 设置图表自适应
|
||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize', function() {
|
||||||
if (northChart) {
|
if (northChart) {
|
||||||
|
@ -68,6 +124,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
console.error('南向资金图表调整大小失败:', e);
|
console.error('南向资金图表调整大小失败:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (rzrqChart) {
|
||||||
|
try {
|
||||||
|
rzrqChart.resize();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('融资融券图表调整大小失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 刷新按钮事件
|
// 刷新按钮事件
|
||||||
|
@ -75,9 +138,56 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (refreshBtn) {
|
if (refreshBtn) {
|
||||||
refreshBtn.addEventListener('click', function() {
|
refreshBtn.addEventListener('click', function() {
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
|
// 如果当前不在交易时段但点击了刷新按钮,显示提示信息
|
||||||
|
if (!isWithinTradingHours()) {
|
||||||
|
showMessage('当前不在交易时段(9:20-16:00),数据可能不会更新');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 融资融券刷新按钮事件
|
||||||
|
const rzrqRefreshBtn = document.getElementById('rzrqRefreshBtn');
|
||||||
|
if (rzrqRefreshBtn) {
|
||||||
|
rzrqRefreshBtn.addEventListener('click', function() {
|
||||||
|
if (rzrqChart) {
|
||||||
|
rzrqChart.showLoading();
|
||||||
|
loadRzrqData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 融资融券指标切换按钮点击事件
|
||||||
|
const metricButtons = document.querySelectorAll('.btn-group button[data-metric]');
|
||||||
|
if (metricButtons && metricButtons.length > 0) {
|
||||||
|
metricButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
metricButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
|
||||||
|
currentMetric = this.getAttribute('data-metric');
|
||||||
|
updateRzrqChart();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示消息提示
|
||||||
|
*/
|
||||||
|
function showMessage(message) {
|
||||||
|
console.log(message);
|
||||||
|
// 可以在这里添加Toast或其他UI提示
|
||||||
|
// 例如:
|
||||||
|
const updateTimeElem = document.getElementById('updateTime');
|
||||||
|
if (updateTimeElem) {
|
||||||
|
const originalText = updateTimeElem.textContent;
|
||||||
|
updateTimeElem.textContent = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
updateTimeElem.textContent = originalText;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载北向和南向资金数据
|
* 加载北向和南向资金数据
|
||||||
*/
|
*/
|
||||||
|
@ -314,7 +424,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// 更新时间
|
// 更新时间
|
||||||
const updateTimeElem = document.getElementById('updateTime');
|
const updateTimeElem = document.getElementById('updateTime');
|
||||||
if (updateTimeElem) {
|
if (updateTimeElem) {
|
||||||
updateTimeElem.textContent = '最后更新时间: ' + data.update_time;
|
let timeText = '最后更新时间: ' + data.update_time;
|
||||||
|
|
||||||
|
// 如果不在交易时段,添加提示
|
||||||
|
if (!isWithinTradingHours()) {
|
||||||
|
timeText += ' (非交易时段)';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeElem.textContent = timeText;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建简单的图表配置
|
// 创建简单的图表配置
|
||||||
|
@ -592,4 +709,603 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
southChart.hideLoading();
|
southChart.hideLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ 融资融券图表相关功能 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化融资融券图表
|
||||||
|
*/
|
||||||
|
function initRzrqChart() {
|
||||||
|
if (rzrqChart) {
|
||||||
|
rzrqChart.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
rzrqChart = echarts.init(document.getElementById('rzrqChart'));
|
||||||
|
|
||||||
|
// 设置图表加载中状态
|
||||||
|
if (rzrqChart) {
|
||||||
|
rzrqChart.showLoading();
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
loadRzrqData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载融资融券数据
|
||||||
|
*/
|
||||||
|
function loadRzrqData() {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/rzrq/chart_data',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
days: 90 // 默认加载90天数据
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.status === 'success') {
|
||||||
|
rzrqData = response.data;
|
||||||
|
rzrqChartData = response.data; // 用于存储融资融券图表的原始数据
|
||||||
|
updateRzrqChart();
|
||||||
|
$('#rzrqUpdateTime').text('数据更新时间: ' + rzrqData.last_update);
|
||||||
|
|
||||||
|
// 初始化融资融券图表的索引选择器
|
||||||
|
if (!rzrqIndexSelector) {
|
||||||
|
initRzrqIndexSelector();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rzrqChart.hideLoading();
|
||||||
|
rzrqChart.setOption({
|
||||||
|
title: {
|
||||||
|
text: '数据加载失败',
|
||||||
|
textStyle: {
|
||||||
|
color: '#999',
|
||||||
|
fontSize: 14
|
||||||
|
},
|
||||||
|
left: 'center',
|
||||||
|
top: 'center'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
rzrqChart.hideLoading();
|
||||||
|
rzrqChart.setOption({
|
||||||
|
title: {
|
||||||
|
text: '数据加载失败: ' + error,
|
||||||
|
textStyle: {
|
||||||
|
color: '#999',
|
||||||
|
fontSize: 14
|
||||||
|
},
|
||||||
|
left: 'center',
|
||||||
|
top: 'center'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化融资融券图表的索引选择器
|
||||||
|
*/
|
||||||
|
function initRzrqIndexSelector() {
|
||||||
|
rzrqIndexSelector = new IndexSelector('rzrqChart', {
|
||||||
|
// 获取图表当前显示的日期范围
|
||||||
|
getDateRange: function() {
|
||||||
|
// 如果有rzrq图表的日期数据,返回第一个和最后一个日期
|
||||||
|
if (rzrqChartData && rzrqChartData.dates && rzrqChartData.dates.length) {
|
||||||
|
return {
|
||||||
|
startDate: rzrqChartData.dates[0],
|
||||||
|
endDate: rzrqChartData.dates[rzrqChartData.dates.length - 1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { startDate: null, endDate: null };
|
||||||
|
},
|
||||||
|
// 指数数据更新时的回调
|
||||||
|
onChange: function(selectedIndices) {
|
||||||
|
updateRzrqChartWithIndices(selectedIndices);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据对齐到日期范围
|
||||||
|
*/
|
||||||
|
function alignDataToDateRange(sourceDates, sourceValues, targetDates) {
|
||||||
|
const result = new Array(targetDates.length).fill(null);
|
||||||
|
const dateMap = {};
|
||||||
|
|
||||||
|
// 创建源数据日期到值的映射
|
||||||
|
sourceDates.forEach((date, index) => {
|
||||||
|
dateMap[date] = sourceValues[index];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 映射到目标日期
|
||||||
|
targetDates.forEach((date, index) => {
|
||||||
|
if (dateMap[date] !== undefined) {
|
||||||
|
result[index] = dateMap[date];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新融资融券图表,添加指数数据
|
||||||
|
*/
|
||||||
|
function updateRzrqChartWithIndices(indices) {
|
||||||
|
if (!rzrqChart) return;
|
||||||
|
|
||||||
|
// 获取当前图表配置
|
||||||
|
const option = rzrqChart.getOption();
|
||||||
|
|
||||||
|
// 保留原始系列数据(融资融券数据)
|
||||||
|
const originalSeries = option.series.filter(s => s.name.indexOf('指数') === -1);
|
||||||
|
|
||||||
|
// 清除所有指数系列
|
||||||
|
option.series = [...originalSeries];
|
||||||
|
|
||||||
|
// 如果没有选择指数,则移除右侧Y轴
|
||||||
|
if (indices.length === 0) {
|
||||||
|
option.yAxis = option.yAxis.filter(axis => axis.name !== '指数值');
|
||||||
|
rzrqChart.setOption(option, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算所有指数数据的最小值和最大值
|
||||||
|
let allValues = [];
|
||||||
|
indices.forEach(index => {
|
||||||
|
if (index.data && index.data.values) {
|
||||||
|
// 过滤掉null和undefined值
|
||||||
|
const validValues = index.data.values.filter(v => v !== null && v !== undefined);
|
||||||
|
allValues = allValues.concat(validValues);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算数据范围
|
||||||
|
let minValue = Math.min(...allValues);
|
||||||
|
let maxValue = Math.max(...allValues);
|
||||||
|
|
||||||
|
// 增加一定的边距,使图表更美观
|
||||||
|
const range = maxValue - minValue;
|
||||||
|
const padding = range * 0.1; // 上下各留10%的边距
|
||||||
|
minValue = minValue - padding;
|
||||||
|
maxValue = maxValue + padding;
|
||||||
|
minValue = Math.round(minValue * 10) / 10;
|
||||||
|
maxValue = Math.round(maxValue * 10) / 10;
|
||||||
|
// 添加指数系列
|
||||||
|
indices.forEach(index => {
|
||||||
|
if (!index.data || !index.data.dates) return;
|
||||||
|
|
||||||
|
// 将指数数据对齐到融资融券数据的日期范围
|
||||||
|
const alignedData = alignDataToDateRange(index.data.dates, index.data.values, option.xAxis[0].data);
|
||||||
|
|
||||||
|
// 创建新的Y轴用于指数
|
||||||
|
if (!option.yAxis.some(axis => axis.name === '指数值')) {
|
||||||
|
option.yAxis.push({
|
||||||
|
name: '指数值',
|
||||||
|
type: 'value',
|
||||||
|
position: 'right',
|
||||||
|
min: minValue, // 使用计算出的最小值
|
||||||
|
max: maxValue, // 使用计算出的最大值
|
||||||
|
splitLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '{value}'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果已存在指数Y轴,更新其范围
|
||||||
|
const indexAxis = option.yAxis.find(axis => axis.name === '指数值');
|
||||||
|
if (indexAxis) {
|
||||||
|
indexAxis.min = minValue;
|
||||||
|
indexAxis.max = maxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加指数系列
|
||||||
|
option.series.push({
|
||||||
|
name: `${index.name}`, // 移除"指数"后缀,避免在tooltip中显示为"上证指数指数"
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 1, // 使用第二个Y轴
|
||||||
|
data: alignedData,
|
||||||
|
symbol: 'none',
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
color: index.color
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: index.color
|
||||||
|
},
|
||||||
|
// 标记这是指数数据
|
||||||
|
isIndex: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新图例
|
||||||
|
option.legend = {
|
||||||
|
data: [
|
||||||
|
...originalSeries.map(s => s.name),
|
||||||
|
...indices.map(i => i.name) // 使用原始指数名称
|
||||||
|
],
|
||||||
|
selected: {
|
||||||
|
...option.legend?.selected || {},
|
||||||
|
...indices.reduce((acc, index) => {
|
||||||
|
acc[index.name] = true; // 使用原始指数名称
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
top: 40 // 将图例下移,避免与标题重叠
|
||||||
|
};
|
||||||
|
|
||||||
|
// 应用更新
|
||||||
|
rzrqChart.setOption(option, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新融资融券图表
|
||||||
|
*/
|
||||||
|
function updateRzrqChart() {
|
||||||
|
if (!rzrqData || !rzrqData.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找当前指标的数据
|
||||||
|
let currentSeries = rzrqData.series.find(s => s.name === getMetricName(currentMetric));
|
||||||
|
|
||||||
|
// 如果未找到,使用第一个系列
|
||||||
|
if (!currentSeries && rzrqData.series.length > 0) {
|
||||||
|
currentSeries = rzrqData.series[0];
|
||||||
|
currentMetric = getMetricKey(currentSeries.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSeries) {
|
||||||
|
rzrqChart.hideLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算数据的最小值和最大值,用于设置Y轴范围
|
||||||
|
const validData = currentSeries.data.filter(value => value !== null && value !== undefined);
|
||||||
|
let min = Math.min(...validData);
|
||||||
|
let max = Math.max(...validData);
|
||||||
|
|
||||||
|
// 为了图表美观,给最小值和最大值增加一些间距
|
||||||
|
const range = max - min;
|
||||||
|
min = min - range * 0.05; // 下方留5%的间距
|
||||||
|
max = max + range * 0.05; // 上方留5%的间距
|
||||||
|
|
||||||
|
// 设置图表选项
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: currentSeries.name + '走势',
|
||||||
|
left: 'center',
|
||||||
|
top: 10 // 固定标题位置在顶部
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: function(params) {
|
||||||
|
let tooltip = params[0].axisValue + '<br/>';
|
||||||
|
params.forEach(param => {
|
||||||
|
// 判断是否为指数系列
|
||||||
|
const isIndexSeries = param.seriesIndex > 0 && param.seriesName.indexOf('融资') === -1 && param.seriesName.indexOf('融券') === -1;
|
||||||
|
|
||||||
|
// 对于指数系列,不添加单位;对于融资融券系列,添加相应单位
|
||||||
|
tooltip += param.marker + ' ' + param.seriesName + ': ' +
|
||||||
|
param.value + (isIndexSeries ? '' : (' ' + currentSeries.unit)) + '<br/>';
|
||||||
|
});
|
||||||
|
return tooltip;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: rzrqData.dates,
|
||||||
|
axisLabel: {
|
||||||
|
rotate: 45
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#999'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: currentSeries.unit,
|
||||||
|
min: min, // 设置Y轴最小值为数据的最小值
|
||||||
|
max: max, // 设置Y轴最大值为数据的最大值
|
||||||
|
nameTextStyle: {
|
||||||
|
padding: [0, 30, 0, 0]
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#999'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#eee'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: currentSeries.name,
|
||||||
|
type: 'line',
|
||||||
|
data: currentSeries.data,
|
||||||
|
lineStyle: {
|
||||||
|
width: 3
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#1890ff'
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: {
|
||||||
|
type: 'linear',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x2: 0,
|
||||||
|
y2: 1,
|
||||||
|
colorStops: [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: 'rgba(24, 144, 255, 0.3)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: 'rgba(24, 144, 255, 0.1)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
connectNulls: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '15%',
|
||||||
|
top: '70px', // 增加顶部空间,给标题和图例留出足够位置
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 40, // 将图例放在标题下方
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rzrqChart.hideLoading();
|
||||||
|
rzrqChart.setOption(option);
|
||||||
|
|
||||||
|
// 更新风险指标显示
|
||||||
|
updateRiskIndicators();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新风险指标显示
|
||||||
|
*/
|
||||||
|
function updateRiskIndicators() {
|
||||||
|
// 检查是否有风险指标数据
|
||||||
|
if (!rzrqData || !rzrqData.risk_indicators) {
|
||||||
|
// 隐藏或显示无数据状态
|
||||||
|
console.log('无风险指标数据');
|
||||||
|
// 清理可能存在的旧数据
|
||||||
|
$('#summaryBalanceChangeDesc').text('--');
|
||||||
|
$('#summarySecuritiesChangeDesc').text('--');
|
||||||
|
$('#overallRiskLevel').text('N/A');
|
||||||
|
$('#overallRiskDesc').text('无法加载风险数据。');
|
||||||
|
$('#overallRiskAlert').removeClass('alert-danger alert-warning alert-success alert-info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indicators = rzrqData.risk_indicators;
|
||||||
|
|
||||||
|
// 辅助函数:格式化描述文本并设置
|
||||||
|
function formatAndSetDescription(targetSelector, descriptionText, defaultText) {
|
||||||
|
const descElement = $(targetSelector);
|
||||||
|
if (!descriptionText) {
|
||||||
|
descElement.text(defaultText || '--');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正则表达式匹配数字(包括正负号和小数点)和百分号
|
||||||
|
const percentageRegex = /([-+]?\d*\.?\d+)%/;
|
||||||
|
const match = descriptionText.match(percentageRegex);
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
const percentageValueStr = match[0]; // 例如 "+0.15%" 或 "-3.14%"
|
||||||
|
const numericValue = parseFloat(match[1]); // 例如 0.15 或 -3.14
|
||||||
|
|
||||||
|
let colorClass = 'neutral';
|
||||||
|
if (numericValue > 0) {
|
||||||
|
colorClass = 'positive';
|
||||||
|
} else if (numericValue < 0) {
|
||||||
|
colorClass = 'negative';
|
||||||
|
}
|
||||||
|
|
||||||
|
const styledDescription = descriptionText.replace(
|
||||||
|
percentageRegex,
|
||||||
|
`<span class="percentage-value ${colorClass}">${percentageValueStr}</span>`
|
||||||
|
);
|
||||||
|
descElement.html(styledDescription);
|
||||||
|
} else {
|
||||||
|
descElement.text(descriptionText); // 没有百分比则直接显示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新综合风险评估
|
||||||
|
if (indicators.overall_risk) {
|
||||||
|
const overallRisk = indicators.overall_risk;
|
||||||
|
const riskLevel = overallRisk.level;
|
||||||
|
|
||||||
|
// 设置风险等级和描述
|
||||||
|
$('#overallRiskLevel').text(riskLevel);
|
||||||
|
$('#overallRiskDesc').text(overallRisk.description);
|
||||||
|
|
||||||
|
// 根据风险等级设置颜色
|
||||||
|
const alertElem = $('#overallRiskAlert');
|
||||||
|
alertElem.removeClass('alert-danger alert-warning alert-success alert-info');
|
||||||
|
|
||||||
|
if (riskLevel === '高') {
|
||||||
|
alertElem.addClass('alert-danger');
|
||||||
|
} else if (riskLevel === '中') {
|
||||||
|
alertElem.addClass('alert-warning');
|
||||||
|
} else if (riskLevel === '低') {
|
||||||
|
alertElem.addClass('alert-success');
|
||||||
|
} else {
|
||||||
|
alertElem.addClass('alert-info');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$('#overallRiskLevel').text('N/A');
|
||||||
|
$('#overallRiskDesc').text('综合风险数据缺失。');
|
||||||
|
$('#overallRiskAlert').removeClass('alert-danger alert-warning alert-success').addClass('alert-info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新融资融券余额变化 - 新的汇总位置
|
||||||
|
if (indicators.balance_risk && indicators.balance_risk.description) {
|
||||||
|
formatAndSetDescription('#summaryBalanceChangeDesc', indicators.balance_risk.description);
|
||||||
|
} else {
|
||||||
|
$('#summaryBalanceChangeDesc').text('融资融券余额变化数据: --');
|
||||||
|
}
|
||||||
|
// 更新卡片内的详细信息(不含移动的描述)
|
||||||
|
if (indicators.recent_balance_change && indicators.balance_risk) {
|
||||||
|
const balanceChange = indicators.recent_balance_change;
|
||||||
|
const balanceRisk = indicators.balance_risk;
|
||||||
|
let rateText = balanceChange.rate > 0 ? '+' : '';
|
||||||
|
rateText += balanceChange.rate + '%';
|
||||||
|
$('#balanceChangeRate').text(rateText).removeClass('text-success text-danger').addClass(balanceChange.rate > 0 ? 'text-danger' : 'text-success');
|
||||||
|
$('#balanceRiskLevel').text(balanceRisk.level);
|
||||||
|
setRiskLevelColor('#balanceRiskLevel', balanceRisk.level);
|
||||||
|
// $('#balanceRiskDesc').text(balanceRisk.description); // 这行被移动了
|
||||||
|
} else {
|
||||||
|
$('#balanceChangeRate').text('--');
|
||||||
|
$('#balanceRiskLevel').text('--');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新融券余额变化 - 新的汇总位置
|
||||||
|
if (indicators.securities_risk && indicators.securities_risk.description) {
|
||||||
|
formatAndSetDescription('#summarySecuritiesChangeDesc', indicators.securities_risk.description);
|
||||||
|
} else {
|
||||||
|
$('#summarySecuritiesChangeDesc').text('融券余额变化数据: --');
|
||||||
|
}
|
||||||
|
// 更新卡片内的详细信息(不含移动的描述)
|
||||||
|
if (indicators.securities_balance_change && indicators.securities_risk) {
|
||||||
|
const securitiesChange = indicators.securities_balance_change;
|
||||||
|
const securitiesRisk = indicators.securities_risk;
|
||||||
|
let rateText = securitiesChange.rate > 0 ? '+' : '';
|
||||||
|
rateText += securitiesChange.rate + '%';
|
||||||
|
$('#securitiesChangeRate').text(rateText).removeClass('text-success text-danger').addClass(securitiesChange.rate > 0 ? 'text-danger' : 'text-success');
|
||||||
|
$('#securitiesRiskLevel').text(securitiesRisk.level);
|
||||||
|
setRiskLevelColor('#securitiesRiskLevel', securitiesRisk.level);
|
||||||
|
// $('#securitiesRiskDesc').text(securitiesRisk.description); // 这行被移动了
|
||||||
|
} else {
|
||||||
|
$('#securitiesChangeRate').text('--');
|
||||||
|
$('#securitiesRiskLevel').text('--');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新融资偿还比率 (这部分逻辑不变,仅为上下文)
|
||||||
|
if (indicators.repay_buy_ratio && indicators.repay_risk) {
|
||||||
|
const repayRatio = indicators.repay_buy_ratio;
|
||||||
|
const repayRisk = indicators.repay_risk;
|
||||||
|
$('#repayBuyRatio').text(repayRatio.value);
|
||||||
|
if (repayRatio.value > 1.1) {
|
||||||
|
$('#repayBuyRatio').removeClass('text-success').addClass('text-danger');
|
||||||
|
} else if (repayRatio.value < 0.9) {
|
||||||
|
$('#repayBuyRatio').removeClass('text-danger').addClass('text-success');
|
||||||
|
} else {
|
||||||
|
$('#repayBuyRatio').removeClass('text-danger text-success');
|
||||||
|
}
|
||||||
|
$('#repayRiskLevel').text(repayRisk.level);
|
||||||
|
setRiskLevelColor('#repayRiskLevel', repayRisk.level);
|
||||||
|
$('#repayRiskDesc').text(repayRisk.description); // 这个描述保留在原位
|
||||||
|
} else {
|
||||||
|
$('#repayBuyRatio').text('--');
|
||||||
|
$('#repayRiskLevel').text('--');
|
||||||
|
$('#repayRiskDesc').text('--');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新融资占比 (这部分逻辑不变,仅为上下文)
|
||||||
|
if (indicators.financing_ratio) {
|
||||||
|
$('#financingRatio').text(indicators.financing_ratio + '%');
|
||||||
|
if (indicators.financing_ratio_percentile !== undefined) {
|
||||||
|
$('#financingRatioPercentile').text(indicators.financing_ratio_percentile + '%');
|
||||||
|
if (indicators.financing_ratio_percentile > 80) {
|
||||||
|
$('#financingRatioPercentile').removeClass('text-success text-warning').addClass('text-danger');
|
||||||
|
} else if (indicators.financing_ratio_percentile > 50) {
|
||||||
|
$('#financingRatioPercentile').removeClass('text-success text-danger').addClass('text-warning');
|
||||||
|
} else {
|
||||||
|
$('#financingRatioPercentile').removeClass('text-danger text-warning').addClass('text-success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$('#financingRatioPercentile').text('数据不足');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$('#financingRatio').text('--');
|
||||||
|
$('#financingRatioPercentile').text('--');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据风险等级设置文本颜色
|
||||||
|
*/
|
||||||
|
function setRiskLevelColor(selector, level) {
|
||||||
|
const elem = $(selector);
|
||||||
|
elem.removeClass('text-danger text-warning text-success text-info');
|
||||||
|
|
||||||
|
if (level === '高') {
|
||||||
|
elem.addClass('text-danger');
|
||||||
|
} else if (level === '中') {
|
||||||
|
elem.addClass('text-warning');
|
||||||
|
} else if (level === '低') {
|
||||||
|
elem.addClass('text-success');
|
||||||
|
} else {
|
||||||
|
elem.addClass('text-info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据数据系列键名获取显示名称
|
||||||
|
*/
|
||||||
|
function getMetricName(metricKey) {
|
||||||
|
const metricMap = {
|
||||||
|
'total_rzrq_balance': '融资融券余额合计',
|
||||||
|
'total_financing_buy': '融资买入额合计',
|
||||||
|
'total_financing_balance': '融资余额合计',
|
||||||
|
'financing_repayment': '融资偿还',
|
||||||
|
'securities_balance': '融券余额'
|
||||||
|
};
|
||||||
|
return metricMap[metricKey] || metricKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据显示名称获取数据系列键名
|
||||||
|
*/
|
||||||
|
function getMetricKey(metricName) {
|
||||||
|
const metricMap = {
|
||||||
|
'融资融券余额合计': 'total_rzrq_balance',
|
||||||
|
'融资买入额合计': 'total_financing_buy',
|
||||||
|
'融资余额合计': 'total_financing_balance',
|
||||||
|
'融资偿还': 'financing_repayment',
|
||||||
|
'融券余额': 'securities_balance'
|
||||||
|
};
|
||||||
|
return metricMap[metricName] || metricName;
|
||||||
|
}
|
||||||
});
|
});
|
|
@ -106,6 +106,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
option.textContent = industry.name;
|
option.textContent = industry.name;
|
||||||
industryNameSelect.appendChild(option);
|
industryNameSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 如果存在Select2,刷新它
|
||||||
|
if ($.fn.select2 && $(industryNameSelect).data('select2')) {
|
||||||
|
$(industryNameSelect).trigger('change');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -734,6 +739,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
industryForm.reset();
|
industryForm.reset();
|
||||||
|
|
||||||
|
// 重置Select2
|
||||||
|
if ($.fn.select2) {
|
||||||
|
$(industryNameSelect).val('').trigger('change');
|
||||||
|
}
|
||||||
|
|
||||||
// 隐藏结果和错误信息
|
// 隐藏结果和错误信息
|
||||||
resultCard.classList.add('d-none');
|
resultCard.classList.add('d-none');
|
||||||
errorAlert.classList.add('d-none');
|
errorAlert.classList.add('d-none');
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -8,8 +8,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const stockCodeInput = document.getElementById('stockCode');
|
const stockCodeInput = document.getElementById('stockCode');
|
||||||
const startDateInput = document.getElementById('startDate');
|
const startDateInput = document.getElementById('startDate');
|
||||||
const metricSelect = document.getElementById('metric');
|
const metricSelect = document.getElementById('metric');
|
||||||
const industryNameInput = document.getElementById('industryName');
|
const industryNameSelect = document.getElementById('industryName');
|
||||||
const conceptNameInput = document.getElementById('conceptName');
|
const conceptNameSelect = document.getElementById('conceptName');
|
||||||
const analyzeBtn = document.getElementById('analyzeBtn');
|
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||||
const resetBtn = document.getElementById('resetBtn');
|
const resetBtn = document.getElementById('resetBtn');
|
||||||
const loadingSpinner = document.getElementById('loadingSpinner');
|
const loadingSpinner = document.getElementById('loadingSpinner');
|
||||||
|
@ -26,6 +26,128 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// 定义图表实例
|
// 定义图表实例
|
||||||
let myChart = null;
|
let myChart = null;
|
||||||
|
|
||||||
|
// 初始化Select2插件
|
||||||
|
$(document).ready(function() {
|
||||||
|
// 初始化行业下拉框
|
||||||
|
$('#industryName').select2({
|
||||||
|
placeholder: '请选择行业',
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化概念板块下拉框
|
||||||
|
$('#conceptName').select2({
|
||||||
|
placeholder: '请选择概念板块',
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载行业数据
|
||||||
|
loadIndustryData();
|
||||||
|
|
||||||
|
// 加载概念板块数据
|
||||||
|
loadConceptData();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载行业数据
|
||||||
|
*/
|
||||||
|
function loadIndustryData() {
|
||||||
|
fetch('/api/industry/list')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取行业列表失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// 先清空下拉框(保留第一个选项)
|
||||||
|
while (industryNameSelect.options.length > 1) {
|
||||||
|
industryNameSelect.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加选项
|
||||||
|
data.data.forEach(industry => {
|
||||||
|
const option = new Option(industry.name, industry.name);
|
||||||
|
industryNameSelect.add(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 刷新Select2
|
||||||
|
$(industryNameSelect).trigger('change');
|
||||||
|
} else {
|
||||||
|
console.error('加载行业数据失败:', data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取行业列表时出错:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载概念板块数据
|
||||||
|
*/
|
||||||
|
function loadConceptData() {
|
||||||
|
fetch('/api/concept/list')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取概念板块列表失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// 先清空下拉框(保留第一个选项)
|
||||||
|
while (conceptNameSelect.options.length > 1) {
|
||||||
|
conceptNameSelect.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加选项
|
||||||
|
data.data.forEach(concept => {
|
||||||
|
const option = new Option(concept.name, concept.name);
|
||||||
|
conceptNameSelect.add(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 刷新Select2
|
||||||
|
$(conceptNameSelect).trigger('change');
|
||||||
|
} else {
|
||||||
|
console.error('加载概念板块数据失败:', data.message);
|
||||||
|
// 加载失败时使用硬编码的常见概念作为备用
|
||||||
|
loadFallbackConcepts();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取概念板块列表时出错:', error);
|
||||||
|
// 出错时使用硬编码的常见概念作为备用
|
||||||
|
loadFallbackConcepts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载备用的概念板块数据(硬编码)
|
||||||
|
*/
|
||||||
|
function loadFallbackConcepts() {
|
||||||
|
const commonConcepts = [
|
||||||
|
"人工智能", "大数据", "云计算", "物联网", "5G", "新能源", "新材料",
|
||||||
|
"生物医药", "半导体", "芯片", "消费电子", "智能汽车", "区块链",
|
||||||
|
"虚拟现实", "元宇宙", "工业互联网", "智能制造", "网络安全", "数字经济"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 先清空下拉框(保留第一个选项)
|
||||||
|
while (conceptNameSelect.options.length > 1) {
|
||||||
|
conceptNameSelect.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加选项
|
||||||
|
commonConcepts.forEach(concept => {
|
||||||
|
const option = new Option(concept, concept);
|
||||||
|
conceptNameSelect.add(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 刷新Select2
|
||||||
|
$(conceptNameSelect).trigger('change');
|
||||||
|
}
|
||||||
|
|
||||||
// 监听表单提交事件
|
// 监听表单提交事件
|
||||||
valuationForm.addEventListener('submit', function(event) {
|
valuationForm.addEventListener('submit', function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -52,8 +174,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const stockCode = stockCodeInput.value.trim();
|
const stockCode = stockCodeInput.value.trim();
|
||||||
const startDate = startDateInput.value;
|
const startDate = startDateInput.value;
|
||||||
const metric = metricSelect.value;
|
const metric = metricSelect.value;
|
||||||
const industryName = industryNameInput.value.trim();
|
const industryName = industryNameSelect.value.trim();
|
||||||
const conceptName = conceptNameInput.value.trim();
|
const conceptName = conceptNameSelect.value.trim();
|
||||||
|
|
||||||
// 构建请求URL
|
// 构建请求URL
|
||||||
let url = `/api/valuation_analysis?stock_code=${stockCode}&start_date=${startDate}&metric=${metric}`;
|
let url = `/api/valuation_analysis?stock_code=${stockCode}&start_date=${startDate}&metric=${metric}`;
|
||||||
|
@ -452,6 +574,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
valuationForm.reset();
|
valuationForm.reset();
|
||||||
|
|
||||||
|
// 重置Select2下拉框
|
||||||
|
$(industryNameSelect).val('').trigger('change');
|
||||||
|
$(conceptNameSelect).val('').trigger('change');
|
||||||
|
|
||||||
// 隐藏结果和错误信息
|
// 隐藏结果和错误信息
|
||||||
resultCard.classList.add('d-none');
|
resultCard.classList.add('d-none');
|
||||||
errorAlert.classList.add('d-none');
|
errorAlert.classList.add('d-none');
|
||||||
|
|
|
@ -6,60 +6,33 @@
|
||||||
<title>沪深港通资金流向监控</title>
|
<title>沪深港通资金流向监控</title>
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link rel="stylesheet" href="../static/css/bootstrap.min.css">
|
<link rel="stylesheet" href="../static/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="../static/css/hsgt_monitor.css">
|
||||||
<!-- 自定义样式 -->
|
<!-- 自定义样式 -->
|
||||||
<style>
|
|
||||||
.card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.card-header {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.money-inflow {
|
|
||||||
color: #d9534f;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.money-outflow {
|
|
||||||
color: #5cb85c;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.chart-container {
|
|
||||||
height: 350px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.stat-card {
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.stat-value {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.stat-title {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.refresh-btn {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.update-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
.flow-direction {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
font-style: italic;
|
|
||||||
margin-top: -5px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- 导航栏 -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="/">股票估值分析工具</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/">个股估值分析</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/industry">行业估值分析</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="/hsgt">资金情况</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
@ -140,21 +113,202 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 说明信息 -->
|
<!-- 融资融券数据汇总信息 -->
|
||||||
|
<div class="row mt-3 mb-2">
|
||||||
|
<div class="col-12 text-center">
|
||||||
|
<p class="summary-text" id="summaryBalanceChangeDesc">--</p>
|
||||||
|
<p class="summary-text" id="summarySecuritiesChangeDesc">--</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 融资融券数据展示 -->
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
数据说明
|
<span>融资融券数据监控 (单位:亿元)</span>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary active" data-metric="total_rzrq_balance">融资融券余额合计</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" data-metric="total_financing_buy">融资买入额合计</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" data-metric="total_financing_balance">融资余额合计</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" data-metric="financing_repayment">融资偿还</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" data-metric="securities_balance">融券余额</button>
|
||||||
|
<button id="rzrqRefreshBtn" class="btn btn-sm btn-outline-secondary ms-2">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> 刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ul>
|
<div id="rzrqChart" class="chart-container"></div>
|
||||||
<li>数据来源:同花顺数据,每分钟更新</li>
|
<p class="update-time text-center" id="rzrqUpdateTime"></p>
|
||||||
<li><strong>北向资金</strong>:是指从<strong>香港</strong>流入<strong>A股</strong>的资金,通过沪股通和深股通进入</li>
|
</div>
|
||||||
<li><strong>南向资金</strong>:是指从<strong>内地</strong>流入<strong>港股</strong>的资金,通过沪市港股通和深市港股通进入</li>
|
</div>
|
||||||
<li>净流入为正表示买入大于卖出,资金流入(<span class="money-inflow">红色</span>);净流入为负表示卖出大于买入,资金流出(<span class="money-outflow">绿色</span>)</li>
|
</div>
|
||||||
<li>交易时间:北向9:30-11:30, 13:00-15:00;南向9:30-12:00, 13:00-16:00</li>
|
</div>
|
||||||
</ul>
|
|
||||||
|
<!-- 融资融券风险分析 -->
|
||||||
|
<!-- <div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
融资融券市场风险分析
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert" id="overallRiskAlert">
|
||||||
|
<h5 class="alert-heading">市场综合风险评估: <span id="overallRiskLevel">加载中...</span></h5>
|
||||||
|
<p id="overallRiskDesc">正在分析融资融券数据,评估市场风险...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
融资融券余额变化
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>变化率:</span>
|
||||||
|
<span id="balanceChangeRate" class="fw-bold">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>风险等级:</span>
|
||||||
|
<span id="balanceRiskLevel">--</span>
|
||||||
|
</div>
|
||||||
|
<p id="balanceRiskDesc" class="mt-2 small text-muted">详细分析如下。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
融资偿还与买入比率
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>平均比率:</span>
|
||||||
|
<span id="repayBuyRatio" class="fw-bold">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>风险等级:</span>
|
||||||
|
<span id="repayRiskLevel">--</span>
|
||||||
|
</div>
|
||||||
|
<p id="repayRiskDesc" class="mt-2 small">--</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
融券余额变化 (空头力量)
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>变化率:</span>
|
||||||
|
<span id="securitiesChangeRate" class="fw-bold">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>风险等级:</span>
|
||||||
|
<span id="securitiesRiskLevel">--</span>
|
||||||
|
</div>
|
||||||
|
<p id="securitiesRiskDesc" class="mt-2 small text-muted">详细分析如下。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
融资占比分析
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>融资占比:</span>
|
||||||
|
<span id="financingRatio" class="fw-bold">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span>历史百分位:</span>
|
||||||
|
<span id="financingRatioPercentile">--</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 small text-muted">融资占比反映了市场中多头使用杠杆的程度。百分位数越高表示融资占比处于历史较高水平。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- 恐贪指数展示 -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>市场恐贪指数 (Fear & Greed Index)</span>
|
||||||
|
<button id="addFearGreedBtn" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> 新增数据
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stat-card" id="fearGreedValue">
|
||||||
|
<div class="stat-value">--</div>
|
||||||
|
<div class="stat-title">最新恐贪指数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stat-card" id="fearGreedStatus">
|
||||||
|
<div class="stat-value">--</div>
|
||||||
|
<div class="stat-title">市场情绪状态</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="stat-card" id="fearGreedDate">
|
||||||
|
<div class="stat-value">--</div>
|
||||||
|
<div class="stat-title">更新日期</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="fearGreedChart" class="chart-container"></div>
|
||||||
|
<p class="update-time text-center" id="fearGreedUpdateTime"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增恐贪指数数据的模态框 -->
|
||||||
|
<div class="modal fade" id="addFearGreedModal" tabindex="-1" aria-labelledby="addFearGreedModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addFearGreedModalLabel">新增恐贪指数数据</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="addFearGreedForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="indexValue" class="form-label">恐贪指数值 (0-100)</label>
|
||||||
|
<input type="number" class="form-control" id="indexValue" min="0" max="100" step="0.01" required>
|
||||||
|
<div class="form-text">输入0-100之间的数值,保留两位小数</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tradingDate" class="form-label">交易日期</label>
|
||||||
|
<input type="date" class="form-control" id="tradingDate" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="submitFearGreed">提交</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,5 +320,544 @@
|
||||||
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="../static/js/echarts.min.js"></script>
|
<script src="../static/js/echarts.min.js"></script>
|
||||||
<script src="../static/js/hsgt_monitor.js"></script>
|
<script src="../static/js/hsgt_monitor.js"></script>
|
||||||
|
|
||||||
|
<!-- 恐贪指数相关的JavaScript -->
|
||||||
|
<script>
|
||||||
|
// 初始化恐贪指数图表
|
||||||
|
let fearGreedChart = null;
|
||||||
|
let fearGreedIndexSelector = null;
|
||||||
|
let fearGreedChartData = null; // 存储恐贪指数数据
|
||||||
|
|
||||||
|
// 加载恐贪指数数据
|
||||||
|
function loadFearGreedData() {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/fear_greed/data',
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.status === 'success') {
|
||||||
|
fearGreedChartData = response.data; // 存储恐贪指数数据
|
||||||
|
updateFearGreedUI(response.data);
|
||||||
|
|
||||||
|
// 初始化恐贪指数选择器,只在首次加载时初始化
|
||||||
|
if (!fearGreedIndexSelector) {
|
||||||
|
initFearGreedIndexSelector();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('加载恐贪指数数据失败:', response.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('请求恐贪指数数据失败:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新恐贪指数UI
|
||||||
|
function updateFearGreedUI(data) {
|
||||||
|
// 更新统计卡片
|
||||||
|
if (data.latest) {
|
||||||
|
$('#fearGreedValue .stat-value').text(data.latest.index_value.toFixed(2));
|
||||||
|
$('#fearGreedStatus .stat-value').text(data.latest_status);
|
||||||
|
$('#fearGreedDate .stat-value').text(data.latest.trading_date);
|
||||||
|
|
||||||
|
// 根据状态设置颜色
|
||||||
|
const value = data.latest.index_value;
|
||||||
|
let statusColor;
|
||||||
|
|
||||||
|
if (value < 25) {
|
||||||
|
statusColor = '#d9534f'; // 红色,极度恐慌
|
||||||
|
} else if (value < 40) {
|
||||||
|
statusColor = '#f0ad4e'; // 橙色,恐慌
|
||||||
|
} else if (value < 50) {
|
||||||
|
statusColor = '#5bc0de'; // 浅蓝色,偏向恐慌
|
||||||
|
} else if (value < 60) {
|
||||||
|
statusColor = '#5cb85c'; // 绿色,中性
|
||||||
|
} else if (value < 75) {
|
||||||
|
statusColor = '#0275d8'; // 蓝色,偏向贪婪
|
||||||
|
} else if (value < 90) {
|
||||||
|
statusColor = '#f0ad4e'; // 橙色,贪婪
|
||||||
|
} else {
|
||||||
|
statusColor = '#d9534f'; // 红色,极度贪婪
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#fearGreedStatus .stat-value').css('color', statusColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新更新时间 - 显示最新数据的更新时间
|
||||||
|
$('#fearGreedUpdateTime').text('最后更新: ' + data.update_time);
|
||||||
|
|
||||||
|
// 初始化/更新图表
|
||||||
|
initFearGreedChart(data.dates, data.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化恐贪指数图表
|
||||||
|
function initFearGreedChart(dates, values) {
|
||||||
|
if (!fearGreedChart) {
|
||||||
|
fearGreedChart = echarts.init(document.getElementById('fearGreedChart'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: function(params) {
|
||||||
|
const param = params[0];
|
||||||
|
const value = param.value;
|
||||||
|
let status;
|
||||||
|
|
||||||
|
if (value < 25) {
|
||||||
|
status = '极度恐慌';
|
||||||
|
} else if (value < 40) {
|
||||||
|
status = '恐慌';
|
||||||
|
} else if (value < 50) {
|
||||||
|
status = '偏向恐慌';
|
||||||
|
} else if (value < 60) {
|
||||||
|
status = '中性';
|
||||||
|
} else if (value < 75) {
|
||||||
|
status = '偏向贪婪';
|
||||||
|
} else if (value < 90) {
|
||||||
|
status = '贪婪';
|
||||||
|
} else {
|
||||||
|
status = '极度贪婪';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${param.axisValue}<br/>恐贪指数: ${value.toFixed(2)}<br/>状态: ${status}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: dates
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '{value}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visualMap: {
|
||||||
|
show: false,
|
||||||
|
dimension: 1,
|
||||||
|
pieces: [
|
||||||
|
{gt: 0, lte: 25, color: '#d9534f'}, // 极度恐慌
|
||||||
|
{gt: 25, lte: 40, color: '#f0ad4e'}, // 恐慌
|
||||||
|
{gt: 40, lte: 50, color: '#5bc0de'}, // 偏向恐慌
|
||||||
|
{gt: 50, lte: 60, color: '#5cb85c'}, // 中性
|
||||||
|
{gt: 60, lte: 75, color: '#0275d8'}, // 偏向贪婪
|
||||||
|
{gt: 75, lte: 90, color: '#f0ad4e'}, // 贪婪
|
||||||
|
{gt: 90, lte: 100, color: '#d9534f'} // 极度贪婪
|
||||||
|
]
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '恐贪指数',
|
||||||
|
type: 'line',
|
||||||
|
data: values,
|
||||||
|
markLine: {
|
||||||
|
silent: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#999'
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
yAxis: 25,
|
||||||
|
label: {
|
||||||
|
formatter: '极度恐慌'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
yAxis: 50,
|
||||||
|
label: {
|
||||||
|
formatter: '中性'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
yAxis: 75,
|
||||||
|
label: {
|
||||||
|
formatter: '贪婪'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
fearGreedChart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化恐贪指数选择器
|
||||||
|
function initFearGreedIndexSelector() {
|
||||||
|
fearGreedIndexSelector = new IndexSelector('fearGreedChart', {
|
||||||
|
// 获取图表当前显示的日期范围
|
||||||
|
getDateRange: function() {
|
||||||
|
// 获取恐贪指数图表的日期范围
|
||||||
|
if (fearGreedChartData && fearGreedChartData.dates && fearGreedChartData.dates.length) {
|
||||||
|
return {
|
||||||
|
startDate: fearGreedChartData.dates[0],
|
||||||
|
endDate: fearGreedChartData.dates[fearGreedChartData.dates.length - 1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { startDate: null, endDate: null };
|
||||||
|
},
|
||||||
|
// 指数数据更新时的回调
|
||||||
|
onChange: function(selectedIndices) {
|
||||||
|
updateFearGreedChartWithIndices(selectedIndices);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将数据对齐到日期范围
|
||||||
|
function alignDataToDateRange(sourceDates, sourceValues, targetDates) {
|
||||||
|
const result = new Array(targetDates.length).fill(null);
|
||||||
|
const dateMap = {};
|
||||||
|
|
||||||
|
// 创建源数据日期到值的映射
|
||||||
|
sourceDates.forEach((date, index) => {
|
||||||
|
dateMap[date] = sourceValues[index];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 映射到目标日期
|
||||||
|
targetDates.forEach((date, index) => {
|
||||||
|
if (dateMap[date] !== undefined) {
|
||||||
|
result[index] = dateMap[date];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表时添加指数数据
|
||||||
|
function updateFearGreedChartWithIndices(indices) {
|
||||||
|
if (!fearGreedChart) return;
|
||||||
|
|
||||||
|
// 获取图表当前选项
|
||||||
|
const option = fearGreedChart.getOption();
|
||||||
|
|
||||||
|
// 保留原始恐贪指数系列
|
||||||
|
const originalSeries = option.series.filter(s => s.name === '恐贪指数');
|
||||||
|
|
||||||
|
// 清除所有系列,并重新添加原始恐贪指数系列
|
||||||
|
option.series = [...originalSeries];
|
||||||
|
|
||||||
|
// 添加指数数据
|
||||||
|
indices.forEach(index => {
|
||||||
|
if (!index.data || !index.data.dates) return;
|
||||||
|
|
||||||
|
// 将指数数据对齐到日期范围
|
||||||
|
const alignedData = alignDataToDateRange(index.data.dates, index.data.values, option.xAxis[0].data);
|
||||||
|
|
||||||
|
// 如果没有第二Y轴,创建新的Y轴用于指数
|
||||||
|
if (!option.yAxis.some(axis => axis.name === '指数值')) {
|
||||||
|
option.yAxis.push({
|
||||||
|
name: '指数值',
|
||||||
|
type: 'value',
|
||||||
|
position: 'right',
|
||||||
|
min: 'dataMin',
|
||||||
|
max: 'dataMax',
|
||||||
|
splitLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '{value}'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新的指数系列
|
||||||
|
option.series.push({
|
||||||
|
name: `${index.name}指数`,
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 1, // 使用第二个Y轴
|
||||||
|
data: alignedData,
|
||||||
|
symbol: 'none',
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
color: index.color
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: index.color
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
|
option.legend = {
|
||||||
|
data: [
|
||||||
|
'恐贪指数',
|
||||||
|
...indices.map(i => `${i.name}指数`)
|
||||||
|
],
|
||||||
|
selected: {
|
||||||
|
'恐贪指数': true,
|
||||||
|
...indices.reduce((acc, index) => {
|
||||||
|
acc[`${index.name}指数`] = true;
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fearGreedChart.setOption(option, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口大小改变时调整图表大小
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
if (fearGreedChart) {
|
||||||
|
fearGreedChart.resize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加恐贪指数数据
|
||||||
|
function addFearGreedData() {
|
||||||
|
const indexValue = parseFloat($('#indexValue').val());
|
||||||
|
const tradingDate = $('#tradingDate').val();
|
||||||
|
|
||||||
|
if (isNaN(indexValue) || indexValue < 0 || indexValue > 100) {
|
||||||
|
alert('请输入有效的恐贪指数值(0-100)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tradingDate) {
|
||||||
|
alert('请选择交易日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/fear_greed/add',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
index_value: indexValue,
|
||||||
|
trading_date: tradingDate
|
||||||
|
}),
|
||||||
|
success: function(response) {
|
||||||
|
if (response.status === 'success') {
|
||||||
|
// 关闭模态框
|
||||||
|
$('#addFearGreedModal').modal('hide');
|
||||||
|
// 重新加载数据
|
||||||
|
loadFearGreedData();
|
||||||
|
// 重置表单
|
||||||
|
$('#addFearGreedForm')[0].reset();
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
// alert('恐贪指数数据添加成功!');
|
||||||
|
} else {
|
||||||
|
alert('添加失败: ' + response.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('添加恐贪指数数据失败:', error);
|
||||||
|
alert('添加失败: ' + error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件绑定
|
||||||
|
$(document).ready(function() {
|
||||||
|
// 加载恐贪指数数据
|
||||||
|
loadFearGreedData();
|
||||||
|
|
||||||
|
// 新增数据按钮点击事件
|
||||||
|
$('#addFearGreedBtn').click(function() {
|
||||||
|
// 设置默认日期为今天
|
||||||
|
$('#tradingDate').val(new Date().toISOString().split('T')[0]);
|
||||||
|
// 显示模态框
|
||||||
|
$('#addFearGreedModal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提交按钮点击事件
|
||||||
|
$('#submitFearGreed').click(function() {
|
||||||
|
addFearGreedData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 刷新按钮点击事件(与恐贪指数一起刷新)
|
||||||
|
$('#refreshBtn').click(function() {
|
||||||
|
loadFearGreedData();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 指数选择器通用组件 -->
|
||||||
|
<script>
|
||||||
|
// 通用指数选择器组件
|
||||||
|
class IndexSelector {
|
||||||
|
constructor(targetChartId, options = {}) {
|
||||||
|
this.targetChartId = targetChartId;
|
||||||
|
this.targetChart = null;
|
||||||
|
this.indices = [];
|
||||||
|
this.selectedIndices = [];
|
||||||
|
this.colors = options.colors || ['#8A2BE2', '#FF1493', '#FF7F50', '#00CED1', '#32CD32'];
|
||||||
|
this.maxIndices = options.maxIndices || 3;
|
||||||
|
this.containerClass = options.containerClass || 'index-selector';
|
||||||
|
this.onChange = options.onChange || (() => {});
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
// 创建选择器DOM元素
|
||||||
|
this.createSelectorDOM();
|
||||||
|
// 加载指数列表
|
||||||
|
this.loadIndicesList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建选择器DOM
|
||||||
|
createSelectorDOM() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = this.containerClass;
|
||||||
|
container.style.cssText = 'margin-left: 10px; display: inline-block;';
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.className = 'form-select form-select-sm';
|
||||||
|
select.id = `${this.targetChartId}-index-select`;
|
||||||
|
select.innerHTML = '<option value="">添加指数叠加...</option>';
|
||||||
|
|
||||||
|
container.appendChild(select);
|
||||||
|
|
||||||
|
// 添加到目标图表的header旁边
|
||||||
|
const chartHeader = document.querySelector(`#${this.targetChartId}`).closest('.card').querySelector('.card-header');
|
||||||
|
const btnGroup = chartHeader.querySelector('.btn-group') || chartHeader;
|
||||||
|
btnGroup.appendChild(container);
|
||||||
|
|
||||||
|
// 添加事件监听
|
||||||
|
select.addEventListener('change', (e) => this.handleSelectChange(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载指数列表
|
||||||
|
loadIndicesList() {
|
||||||
|
fetch('/api/indices/list')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
this.indices = data.data;
|
||||||
|
this.updateSelectOptions();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('加载指数列表失败:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新下拉选项
|
||||||
|
updateSelectOptions() {
|
||||||
|
const select = document.getElementById(`${this.targetChartId}-index-select`);
|
||||||
|
// 保留第一个选项
|
||||||
|
select.innerHTML = '<option value="">添加指数叠加...</option>';
|
||||||
|
|
||||||
|
// 添加未选择的指数到下拉列表
|
||||||
|
this.indices.forEach(index => {
|
||||||
|
if (!this.selectedIndices.some(i => i.code === index.code)) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = index.code;
|
||||||
|
option.textContent = index.name;
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理选择变更
|
||||||
|
handleSelectChange(e) {
|
||||||
|
const indexCode = e.target.value;
|
||||||
|
if (!indexCode) return;
|
||||||
|
|
||||||
|
const index = this.indices.find(i => i.code === indexCode);
|
||||||
|
if (!index) return;
|
||||||
|
|
||||||
|
// 检查选择的指数数量限制
|
||||||
|
if (this.selectedIndices.length >= this.maxIndices) {
|
||||||
|
alert(`最多只能叠加${this.maxIndices}个指数!`);
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到已选择列表
|
||||||
|
const colorIndex = this.selectedIndices.length % this.colors.length;
|
||||||
|
const selectedIndex = {
|
||||||
|
...index,
|
||||||
|
color: this.colors[colorIndex],
|
||||||
|
visible: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectedIndices.push(selectedIndex);
|
||||||
|
|
||||||
|
// 添加指数标签
|
||||||
|
this.addIndexLabel(selectedIndex);
|
||||||
|
|
||||||
|
// 加载并显示指数数据
|
||||||
|
this.loadIndexData(selectedIndex);
|
||||||
|
|
||||||
|
// 重置选择框
|
||||||
|
e.target.value = '';
|
||||||
|
this.updateSelectOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加指数标签
|
||||||
|
addIndexLabel(index) {
|
||||||
|
const container = document.querySelector(`#${this.targetChartId}`).closest('.card-body');
|
||||||
|
const labelsContainer = container.querySelector('.index-labels') || (() => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'index-labels d-flex flex-wrap mt-2';
|
||||||
|
container.insertBefore(div, container.firstChild);
|
||||||
|
return div;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'badge bg-light text-dark me-2 mb-2 p-2 d-flex align-items-center';
|
||||||
|
label.style.borderLeft = `3px solid ${index.color}`;
|
||||||
|
label.innerHTML = `
|
||||||
|
<span class="me-2">${index.name}</span>
|
||||||
|
<button type="button" class="btn-close btn-close-sm" aria-label="移除"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
label.querySelector('.btn-close').addEventListener('click', () => {
|
||||||
|
this.removeIndex(index.code);
|
||||||
|
labelsContainer.removeChild(label);
|
||||||
|
});
|
||||||
|
|
||||||
|
labelsContainer.appendChild(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载指数数据
|
||||||
|
loadIndexData(index) {
|
||||||
|
// 获取图表当前日期范围
|
||||||
|
const chartDates = this.options.getDateRange ?
|
||||||
|
this.options.getDateRange() :
|
||||||
|
{ startDate: null, endDate: null };
|
||||||
|
|
||||||
|
fetch(`/api/indices/data?code=${index.code}&start_date=${chartDates.startDate || ''}&end_date=${chartDates.endDate || ''}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// 更新指数数据
|
||||||
|
index.data = data.data;
|
||||||
|
|
||||||
|
// 调用回调函数更新图表
|
||||||
|
this.onChange(this.selectedIndices);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error(`加载指数 ${index.name} 数据失败:`, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除指数
|
||||||
|
removeIndex(indexCode) {
|
||||||
|
const index = this.selectedIndices.findIndex(i => i.code === indexCode);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.selectedIndices.splice(index, 1);
|
||||||
|
this.updateSelectOptions();
|
||||||
|
|
||||||
|
// 调用回调函数更新图表
|
||||||
|
this.onChange(this.selectedIndices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有已选择的指数
|
||||||
|
getSelectedIndices() {
|
||||||
|
return [...this.selectedIndices];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -10,8 +10,25 @@
|
||||||
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<!-- 引入ECharts -->
|
<!-- 引入ECharts -->
|
||||||
<script src="../static/js/echarts.min.js"></script>
|
<script src="../static/js/echarts.min.js"></script>
|
||||||
|
<!-- 引入Select2 CSS 和 JS (用于可搜索的下拉框) -->
|
||||||
|
<link href="../static/css/select2.min.css" rel="stylesheet" />
|
||||||
|
<script src="../static/js/select2.min.js"></script>
|
||||||
<!-- 引入自定义CSS -->
|
<!-- 引入自定义CSS -->
|
||||||
<link href="/static/css/style.css" rel="stylesheet">
|
<link href="/static/css/style.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* 自定义Select2样式使其与Bootstrap兼容 */
|
||||||
|
.select2-container .select2-selection--single {
|
||||||
|
height: 38px;
|
||||||
|
line-height: 38px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||||
|
line-height: 38px;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏 -->
|
||||||
|
@ -29,6 +46,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/industry">行业估值分析</a>
|
<a class="nav-link" href="/industry">行业估值分析</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/hsgt">资金情况</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,12 +84,18 @@
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="industryName" class="form-label">行业名称(可选)</label>
|
<label for="industryName" class="form-label">行业名称(可选)</label>
|
||||||
<input type="text" class="form-control" id="industryName" placeholder="例如: 半导体">
|
<select class="form-control select2" id="industryName">
|
||||||
|
<option value="">请选择行业</option>
|
||||||
|
<!-- 选项将通过JS动态加载 -->
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="conceptName" class="form-label">概念板块(可选)</label>
|
<label for="conceptName" class="form-label">概念板块(可选)</label>
|
||||||
<input type="text" class="form-control" id="conceptName" placeholder="例如: 人工智能">
|
<select class="form-control select2" id="conceptName">
|
||||||
|
<option value="">请选择概念板块</option>
|
||||||
|
<!-- 选项将通过JS动态加载 -->
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 text-center mt-4">
|
<div class="col-12 text-center mt-4">
|
||||||
|
@ -148,5 +174,39 @@
|
||||||
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
||||||
<!-- 引入自定义JS -->
|
<!-- 引入自定义JS -->
|
||||||
<script src="/static/js/valuation.js"></script>
|
<script src="/static/js/valuation.js"></script>
|
||||||
|
<script>
|
||||||
|
// 行业和概念板块互斥选择逻辑
|
||||||
|
$(document).ready(function() {
|
||||||
|
// 行业选择变化时
|
||||||
|
$('#industryName').on('change', function() {
|
||||||
|
if ($(this).val()) {
|
||||||
|
// 如果选择了行业,禁用概念板块
|
||||||
|
$('#conceptName').prop('disabled', true);
|
||||||
|
// 同时更新Select2的状态
|
||||||
|
$('#conceptName').select2({disabled: true});
|
||||||
|
} else {
|
||||||
|
// 如果清空了行业,启用概念板块
|
||||||
|
$('#conceptName').prop('disabled', false);
|
||||||
|
// 同时更新Select2的状态
|
||||||
|
$('#conceptName').select2({disabled: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 概念板块选择变化时
|
||||||
|
$('#conceptName').on('change', function() {
|
||||||
|
if ($(this).val()) {
|
||||||
|
// 如果选择了概念,禁用行业
|
||||||
|
$('#industryName').prop('disabled', true);
|
||||||
|
// 同时更新Select2的状态
|
||||||
|
$('#industryName').select2({disabled: true});
|
||||||
|
} else {
|
||||||
|
// 如果清空了概念,启用行业
|
||||||
|
$('#industryName').prop('disabled', false);
|
||||||
|
// 同时更新Select2的状态
|
||||||
|
$('#industryName').select2({disabled: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -10,8 +10,25 @@
|
||||||
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<!-- 引入ECharts -->
|
<!-- 引入ECharts -->
|
||||||
<script src="../static/js/echarts.min.js"></script>
|
<script src="../static/js/echarts.min.js"></script>
|
||||||
|
<!-- 引入Select2 CSS 和 JS (用于可搜索的下拉框) -->
|
||||||
|
<link href="../static/css/select2.min.css" rel="stylesheet" />
|
||||||
|
<script src="../static/js/select2.min.js"></script>
|
||||||
<!-- 引入自定义CSS -->
|
<!-- 引入自定义CSS -->
|
||||||
<link href="/static/css/style.css" rel="stylesheet">
|
<link href="/static/css/style.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* 自定义Select2样式使其与Bootstrap兼容 */
|
||||||
|
.select2-container .select2-selection--single {
|
||||||
|
height: 38px;
|
||||||
|
line-height: 38px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||||
|
line-height: 38px;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏 -->
|
||||||
|
@ -29,6 +46,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" href="/industry">行业估值分析</a>
|
<a class="nav-link active" href="/industry">行业估值分析</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/hsgt">资金情况</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +65,7 @@
|
||||||
<form id="industryForm" class="row g-3">
|
<form id="industryForm" class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label for="industryName" class="form-label">行业名称</label>
|
<label for="industryName" class="form-label">行业名称</label>
|
||||||
<select class="form-select" id="industryName" required>
|
<select class="form-select select2" id="industryName" required>
|
||||||
<option value="" selected disabled>请选择行业</option>
|
<option value="" selected disabled>请选择行业</option>
|
||||||
<!-- 将通过API动态填充 -->
|
<!-- 将通过API动态填充 -->
|
||||||
</select>
|
</select>
|
||||||
|
@ -165,5 +185,21 @@
|
||||||
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
||||||
<!-- 引入行业分析JS -->
|
<!-- 引入行业分析JS -->
|
||||||
<script src="/static/js/industry.js"></script>
|
<script src="/static/js/industry.js"></script>
|
||||||
|
<script>
|
||||||
|
// 初始化Select2
|
||||||
|
$(document).ready(function() {
|
||||||
|
// 初始化行业下拉框为可搜索
|
||||||
|
$('#industryName').select2({
|
||||||
|
placeholder: '请选择行业',
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置表单时也需要重置Select2
|
||||||
|
$('#resetBtn').on('click', function() {
|
||||||
|
$('#industryName').val('').trigger('change');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,76 @@
|
||||||
|
import redis
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DistributedLock:
|
||||||
|
def __init__(self, redis_client: redis.Redis, lock_name: str, expire_time: int = 60):
|
||||||
|
"""
|
||||||
|
初始化分布式锁
|
||||||
|
|
||||||
|
Args:
|
||||||
|
redis_client: Redis客户端实例
|
||||||
|
lock_name: 锁的名称
|
||||||
|
expire_time: 锁的过期时间(秒)
|
||||||
|
"""
|
||||||
|
self.redis_client = redis_client
|
||||||
|
self.lock_name = f"lock:{lock_name}"
|
||||||
|
self.expire_time = expire_time
|
||||||
|
|
||||||
|
def acquire(self) -> bool:
|
||||||
|
"""
|
||||||
|
尝试获取锁
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功获取锁
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用SETNX命令尝试获取锁
|
||||||
|
# 如果key不存在,则设置key的值为当前时间戳,并设置过期时间
|
||||||
|
current_time = int(time.time())
|
||||||
|
result = self.redis_client.set(
|
||||||
|
self.lock_name,
|
||||||
|
current_time,
|
||||||
|
ex=self.expire_time,
|
||||||
|
nx=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info(f"成功获取锁: {self.lock_name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.info(f"未能获取锁: {self.lock_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取锁时发生错误: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def release(self) -> bool:
|
||||||
|
"""
|
||||||
|
释放锁
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功释放锁
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 先检查锁是否存在
|
||||||
|
lock_value = self.redis_client.get(self.lock_name)
|
||||||
|
if not lock_value:
|
||||||
|
logger.info(f"锁不存在: {self.lock_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 删除锁
|
||||||
|
result = self.redis_client.delete(self.lock_name)
|
||||||
|
if result:
|
||||||
|
logger.info(f"成功释放锁: {self.lock_name}, 原值: {lock_value}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"释放锁失败: {self.lock_name}, 原值: {lock_value}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"释放锁时发生错误: {e}")
|
||||||
|
return False
|
|
@ -1,111 +1,185 @@
|
||||||
"""
|
"""
|
||||||
PE/PB估值分析命令行工具
|
估值分析模块命令行工具
|
||||||
|
|
||||||
使用方法:
|
|
||||||
python -m src.valuation_analysis.cli --stock 601138
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
import sys
|
||||||
from typing import Optional, List, Dict
|
import os
|
||||||
|
|
||||||
from .pe_pb_analysis import ValuationAnalyzer, analyze_stock
|
|
||||||
from .config import DB_URL, OUTPUT_DIR
|
|
||||||
|
|
||||||
logger = logging.getLogger("valuation_analysis.cli")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
|
||||||
"""解析命令行参数"""
|
|
||||||
parser = argparse.ArgumentParser(description="股票PE/PB估值分析工具")
|
|
||||||
|
|
||||||
parser.add_argument('--stock', '-s', type=str, required=True,
|
|
||||||
help='股票代码,例如:601138')
|
|
||||||
parser.add_argument('--start-date', type=str, default='2018-01-01',
|
|
||||||
help='起始日期 (默认: 2018-01-01)')
|
|
||||||
parser.add_argument('--metrics', type=str, default='pe,pb',
|
|
||||||
help='分析指标,用逗号分隔 (默认: pe,pb)')
|
|
||||||
parser.add_argument('--output', '-o', type=str, default=None,
|
|
||||||
help='结果保存路径 (默认: results/valuation_analysis/)')
|
|
||||||
parser.add_argument('--format', type=str, choices=['json', 'text'], default='text',
|
|
||||||
help='输出格式 (默认: text)')
|
|
||||||
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
from . import pe_pb_analysis
|
||||||
|
from . import industry_analysis
|
||||||
|
from . import rzrq_collector
|
||||||
|
from .config import OUTPUT_DIR
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
"""命令行工具主函数"""
|
||||||
args = parse_args()
|
|
||||||
|
|
||||||
# 解析参数
|
parser = argparse.ArgumentParser(description='股票估值分析工具', formatter_class=argparse.RawTextHelpFormatter)
|
||||||
stock_code = args.stock
|
subparsers = parser.add_subparsers(dest='command', help='子命令')
|
||||||
start_date = args.start_date
|
|
||||||
metrics = args.metrics.split(',')
|
|
||||||
output_format = args.format
|
|
||||||
|
|
||||||
# 设置输出路径
|
# 设置PE/PB分析子命令
|
||||||
output_path = args.output
|
pepb_parser = subparsers.add_parser('pepb', help='PE/PB分析')
|
||||||
if output_path is None:
|
pepb_parser.add_argument('--stock', '-s', required=True, help='股票代码')
|
||||||
output_path = OUTPUT_DIR / f"{stock_code}_valuation_analysis.{output_format}"
|
pepb_parser.add_argument('--days', '-d', type=int, default=1000, help='分析天数 (默认: 1000)')
|
||||||
else:
|
pepb_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json',
|
||||||
output_path = Path(output_path)
|
help='输出格式 (默认: json)')
|
||||||
|
|
||||||
# 运行分析
|
# 设置ROE分析子命令
|
||||||
analyzer = ValuationAnalyzer()
|
roe_parser = subparsers.add_parser('roe', help='ROE分析')
|
||||||
result = analyzer.analyze_stock_valuation(stock_code, start_date, metrics)
|
roe_parser.add_argument('--stock', '-s', required=True, help='股票代码')
|
||||||
|
roe_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json',
|
||||||
|
help='输出格式 (默认: json)')
|
||||||
|
|
||||||
# 输出结果
|
# 设置行业分析子命令
|
||||||
if not result['success']:
|
industry_parser = subparsers.add_parser('industry', help='行业估值分析')
|
||||||
print(f"分析失败: {result.get('message', '未知错误')}")
|
industry_parser.add_argument('--name', '-n', required=True, help='行业名称')
|
||||||
return 1
|
industry_parser.add_argument('--metric', '-m', choices=['pe', 'pb', 'ps'], default='pe',
|
||||||
|
help='估值指标 (pe/pb/ps) (默认: pe)')
|
||||||
|
industry_parser.add_argument('--days', '-d', type=int, default=1095,
|
||||||
|
help='分析天数 (默认: 1095,约3年)')
|
||||||
|
industry_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json',
|
||||||
|
help='输出格式 (默认: json)')
|
||||||
|
|
||||||
# 打印分析结果
|
# 设置行业列表子命令
|
||||||
stock_name = result['stock_name']
|
industry_list_parser = subparsers.add_parser('industry-list', help='获取行业列表')
|
||||||
analysis_date = result['analysis_date']
|
|
||||||
|
|
||||||
if output_format == 'json':
|
# 设置融资融券数据采集子命令
|
||||||
# 将图表路径转换为相对路径字符串
|
rzrq_parser = subparsers.add_parser('rzrq', help='融资融券数据采集')
|
||||||
for metric in result['metrics']:
|
rzrq_parser.add_argument('--action', '-a', choices=['init', 'update', 'run-scheduler'],
|
||||||
if 'chart_path' in result['metrics'][metric]:
|
required=True, help='操作类型: init-首次全量采集,update-更新最新数据,run-scheduler-运行定时器')
|
||||||
result['metrics'][metric]['chart_path'] = str(result['metrics'][metric]['chart_path'])
|
rzrq_parser.add_argument('--output-sql', '-s', action='store_true',
|
||||||
|
help='输出创建表的SQL语句,仅与init配合使用')
|
||||||
|
|
||||||
# 写入JSON文件
|
# 解析命令行参数
|
||||||
with open(output_path, 'w', encoding='utf-8') as f:
|
args = parser.parse_args()
|
||||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
print(f"分析结果已保存至: {output_path}")
|
# 如果没有提供子命令,显示帮助信息
|
||||||
else:
|
if not args.command:
|
||||||
# 打印文本格式分析结果
|
parser.print_help()
|
||||||
print("\n" + "="*50)
|
sys.exit(1)
|
||||||
print(f"股票代码: {stock_code}")
|
|
||||||
print(f"股票名称: {stock_name}")
|
|
||||||
print(f"分析日期: {analysis_date}")
|
|
||||||
print("="*50)
|
|
||||||
|
|
||||||
for metric in result['metrics']:
|
# 执行对应的子命令
|
||||||
metric_data = result['metrics'][metric]
|
if args.command == 'pepb':
|
||||||
metric_name = "PE" if metric == "pe" else "PB"
|
# PE/PB分析
|
||||||
|
start_date = (datetime.datetime.now() - datetime.timedelta(days=args.days)).strftime('%Y-%m-%d')
|
||||||
|
end_date = datetime.datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
print(f"\n{metric_name}分析结果:")
|
analyzer = pe_pb_analysis.StockValuationAnalyzer()
|
||||||
print("-"*30)
|
result = analyzer.get_stock_pe_pb_analysis(args.stock, start_date, end_date)
|
||||||
print(f"当前{metric_name}: {metric_data['current']:.2f}")
|
|
||||||
print(f"{metric_name}百分位: {metric_data['percentile']:.2f}%")
|
|
||||||
print(f"历史最小值: {metric_data['min']:.2f}")
|
|
||||||
print(f"历史最大值: {metric_data['max']:.2f}")
|
|
||||||
print(f"历史均值: {metric_data['mean']:.2f}")
|
|
||||||
print(f"历史中位数: {metric_data['median']:.2f}")
|
|
||||||
print(f"第一四分位数: {metric_data['q1']:.2f}")
|
|
||||||
print(f"第三四分位数: {metric_data['q3']:.2f}")
|
|
||||||
print(f"估值曲线图: {metric_data['chart_path']}")
|
|
||||||
|
|
||||||
print("\n" + "="*50)
|
if result["success"]:
|
||||||
print(f"分析完成,图表已保存")
|
# 输出结果
|
||||||
|
if args.output in ['json', 'all']:
|
||||||
|
output_file = os.path.join(OUTPUT_DIR, f"{args.stock}_pepb_analysis.json")
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"结果已保存到: {output_file}")
|
||||||
|
|
||||||
return 0
|
if args.output in ['csv', 'all']:
|
||||||
|
# 这里可以添加CSV输出逻辑
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 打印简要结果
|
||||||
|
print(f"\n{args.stock} PE/PB分析结果:")
|
||||||
|
print(f"当前PE: {result['pe']['current']:.2f}, 百分位: {result['pe']['percentile']:.2f}%")
|
||||||
|
print(f"当前PB: {result['pb']['current']:.2f}, 百分位: {result['pb']['percentile']:.2f}%")
|
||||||
|
else:
|
||||||
|
print(f"分析失败: {result['message']}")
|
||||||
|
|
||||||
|
elif args.command == 'roe':
|
||||||
|
# ROE分析
|
||||||
|
analyzer = pe_pb_analysis.StockValuationAnalyzer()
|
||||||
|
result = analyzer.get_stock_roe_analysis(args.stock)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
# 输出结果
|
||||||
|
if args.output in ['json', 'all']:
|
||||||
|
output_file = os.path.join(OUTPUT_DIR, f"{args.stock}_roe_analysis.json")
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"结果已保存到: {output_file}")
|
||||||
|
|
||||||
|
# 打印简要结果
|
||||||
|
print(f"\n{args.stock} ROE分析结果:")
|
||||||
|
print(f"最新ROE: {result['latest_roe']:.2f}%")
|
||||||
|
print(f"5年平均ROE: {result['avg_5year_roe']:.2f}%")
|
||||||
|
else:
|
||||||
|
print(f"分析失败: {result['message']}")
|
||||||
|
|
||||||
|
elif args.command == 'industry':
|
||||||
|
# 行业估值分析
|
||||||
|
start_date = (datetime.datetime.now() - datetime.timedelta(days=args.days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
analyzer = industry_analysis.IndustryAnalyzer()
|
||||||
|
result = analyzer.get_industry_analysis(args.name, args.metric, start_date)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
# 输出结果
|
||||||
|
if args.output in ['json', 'all']:
|
||||||
|
output_file = os.path.join(OUTPUT_DIR, f"{args.name}_{args.metric}_analysis.json")
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"结果已保存到: {output_file}")
|
||||||
|
|
||||||
|
# 打印简要结果
|
||||||
|
print(f"\n{args.name} {args.metric.upper()}分析结果:")
|
||||||
|
current = result['valuation']['percentiles']['current']
|
||||||
|
percentile = result['valuation']['percentiles']['percentile']
|
||||||
|
print(f"当前{args.metric.upper()}: {current:.2f}, 百分位: {percentile:.2f}%")
|
||||||
|
if "crowding" in result:
|
||||||
|
crowding_level = result['crowding']['current']['level']
|
||||||
|
crowding_percentile = result['crowding']['current']['percentile']
|
||||||
|
print(f"行业拥挤度: {crowding_level} ({crowding_percentile:.2f}%)")
|
||||||
|
else:
|
||||||
|
print(f"分析失败: {result['message']}")
|
||||||
|
|
||||||
|
elif args.command == 'industry-list':
|
||||||
|
# 获取行业列表
|
||||||
|
analyzer = industry_analysis.IndustryAnalyzer()
|
||||||
|
industry_list = analyzer.get_industry_list()
|
||||||
|
|
||||||
|
if industry_list:
|
||||||
|
for i, industry in enumerate(industry_list, 1):
|
||||||
|
print(f"{i}. {industry['name']} ({industry['code']})")
|
||||||
|
else:
|
||||||
|
print("获取行业列表失败")
|
||||||
|
|
||||||
|
elif args.command == 'rzrq':
|
||||||
|
# 融资融券数据采集
|
||||||
|
collector = rzrq_collector.RzrqCollector()
|
||||||
|
|
||||||
|
if args.action == 'init':
|
||||||
|
# 输出建表SQL
|
||||||
|
if args.output_sql:
|
||||||
|
print("创建融资融券数据表的SQL语句:")
|
||||||
|
print(rzrq_collector.get_create_table_sql())
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
# 首次全量采集
|
||||||
|
print("开始首次全量采集融资融券数据...")
|
||||||
|
result = collector.initial_data_collection()
|
||||||
|
if result:
|
||||||
|
print("融资融券数据采集完成")
|
||||||
|
else:
|
||||||
|
print("融资融券数据采集失败")
|
||||||
|
|
||||||
|
elif args.action == 'update':
|
||||||
|
# 更新最新数据
|
||||||
|
print("开始更新最新融资融券数据...")
|
||||||
|
result = collector.update_latest_data()
|
||||||
|
if result:
|
||||||
|
print("融资融券数据更新完成")
|
||||||
|
else:
|
||||||
|
print("融资融券数据更新失败")
|
||||||
|
|
||||||
|
elif args.action == 'run-scheduler':
|
||||||
|
# 运行定时器
|
||||||
|
print("启动融资融券数据采集定时器,将在每天下午17:00自动更新...")
|
||||||
|
print("按Ctrl+C终止")
|
||||||
|
collector.schedule_daily_update()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
main()
|
|
@ -0,0 +1,863 @@
|
||||||
|
"""
|
||||||
|
东方财富融资融券数据采集模块
|
||||||
|
提供从东方财富网站采集融资融券数据并存储到数据库的功能
|
||||||
|
功能包括:
|
||||||
|
1. 采集融资融券数据
|
||||||
|
2. 存储数据到数据库
|
||||||
|
3. 定时自动更新数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
|
||||||
|
# 添加项目根目录到Python路径
|
||||||
|
current_file = Path(__file__)
|
||||||
|
project_root = current_file.parent.parent.parent
|
||||||
|
sys.path.append(str(project_root))
|
||||||
|
|
||||||
|
from src.valuation_analysis.config import DB_URL, LOG_FILE
|
||||||
|
|
||||||
|
# 获取项目根目录
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
# 确保日志目录存在
|
||||||
|
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("eastmoney_rzrq_collector")
|
||||||
|
|
||||||
|
|
||||||
|
class EastmoneyRzrqCollector:
|
||||||
|
"""东方财富融资融券数据采集器类"""
|
||||||
|
|
||||||
|
def __init__(self, db_url: str = DB_URL):
|
||||||
|
"""
|
||||||
|
初始化东方财富融资融券数据采集器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_url: 数据库连接URL
|
||||||
|
"""
|
||||||
|
self.engine = create_engine(
|
||||||
|
db_url,
|
||||||
|
pool_size=5,
|
||||||
|
max_overflow=10,
|
||||||
|
pool_recycle=3600
|
||||||
|
)
|
||||||
|
self.base_url = "https://datacenter-web.eastmoney.com/api/data/v1/get"
|
||||||
|
self.headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||||
|
"Accept": "application/json, text/plain, */*",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||||
|
"Origin": "https://data.eastmoney.com",
|
||||||
|
"Referer": "https://data.eastmoney.com/",
|
||||||
|
}
|
||||||
|
logger.info("东方财富融资融券数据采集器初始化完成")
|
||||||
|
|
||||||
|
def _parse_date(self, date_str: str) -> datetime.date:
|
||||||
|
"""将日期字符串解析为日期对象"""
|
||||||
|
if not date_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(date_str.split()[0], "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"日期解析失败: {date_str}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _ensure_table_exists(self) -> bool:
|
||||||
|
"""
|
||||||
|
确保数据表存在,如果不存在则创建
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功确保表存在
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
create_table_query = text("""
|
||||||
|
CREATE TABLE IF NOT EXISTS eastmoney_rzrq_data (
|
||||||
|
trade_date DATE PRIMARY KEY,
|
||||||
|
index_value DECIMAL(10,4) COMMENT '指数',
|
||||||
|
change_percent DECIMAL(10,4) COMMENT '涨跌幅',
|
||||||
|
float_market_value DECIMAL(20,2) COMMENT '流通市值',
|
||||||
|
change_percent_3d DECIMAL(10,4) COMMENT '3日涨跌幅',
|
||||||
|
change_percent_5d DECIMAL(10,4) COMMENT '5日涨跌幅',
|
||||||
|
change_percent_10d DECIMAL(10,4) COMMENT '10日涨跌幅',
|
||||||
|
financing_balance DECIMAL(20,2) COMMENT '融资余额',
|
||||||
|
financing_balance_ratio DECIMAL(10,4) COMMENT '融资余额占比',
|
||||||
|
financing_buy_amount DECIMAL(20,2) COMMENT '融资买入额',
|
||||||
|
financing_buy_amount_3d DECIMAL(20,2) COMMENT '3日融资买入额',
|
||||||
|
financing_buy_amount_5d DECIMAL(20,2) COMMENT '5日融资买入额',
|
||||||
|
financing_buy_amount_10d DECIMAL(20,2) COMMENT '10日融资买入额',
|
||||||
|
financing_repay_amount DECIMAL(20,2) COMMENT '融资偿还额',
|
||||||
|
financing_repay_amount_3d DECIMAL(20,2) COMMENT '3日融资偿还额',
|
||||||
|
financing_repay_amount_5d DECIMAL(20,2) COMMENT '5日融资偿还额',
|
||||||
|
financing_repay_amount_10d DECIMAL(20,2) COMMENT '10日融资偿还额',
|
||||||
|
financing_net_amount DECIMAL(20,2) COMMENT '融资净额',
|
||||||
|
financing_net_amount_3d DECIMAL(20,2) COMMENT '3日融资净额',
|
||||||
|
financing_net_amount_5d DECIMAL(20,2) COMMENT '5日融资净额',
|
||||||
|
financing_net_amount_10d DECIMAL(20,2) COMMENT '10日融资净额',
|
||||||
|
securities_balance DECIMAL(20,2) COMMENT '融券余额',
|
||||||
|
securities_volume DECIMAL(20,2) COMMENT '融券余量',
|
||||||
|
securities_repay_volume DECIMAL(20,2) COMMENT '融券偿还量',
|
||||||
|
securities_repay_volume_3d DECIMAL(20,2) COMMENT '3日融券偿还量',
|
||||||
|
securities_repay_volume_5d DECIMAL(20,2) COMMENT '5日融券偿还量',
|
||||||
|
securities_repay_volume_10d DECIMAL(20,2) COMMENT '10日融券偿还量',
|
||||||
|
securities_sell_volume DECIMAL(20,2) COMMENT '融券卖出量',
|
||||||
|
securities_sell_volume_3d DECIMAL(20,2) COMMENT '3日融券卖出量',
|
||||||
|
securities_sell_volume_5d DECIMAL(20,2) COMMENT '5日融券卖出量',
|
||||||
|
securities_sell_volume_10d DECIMAL(20,2) COMMENT '10日融券卖出量',
|
||||||
|
securities_net_volume DECIMAL(20,2) COMMENT '融券净量',
|
||||||
|
securities_net_volume_3d DECIMAL(20,2) COMMENT '3日融券净量',
|
||||||
|
securities_net_volume_5d DECIMAL(20,2) COMMENT '5日融券净量',
|
||||||
|
securities_net_volume_10d DECIMAL(20,2) COMMENT '10日融券净量',
|
||||||
|
total_rzrq_balance DECIMAL(20,2) COMMENT '融资融券余额',
|
||||||
|
total_rzrq_balance_cz DECIMAL(20,2) COMMENT '融资融券余额差值',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='东方财富融资融券数据表';
|
||||||
|
""")
|
||||||
|
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
conn.execute(create_table_query)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
logger.info("东方财富融资融券数据表创建成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"确保数据表存在失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fetch_data(self, page: int = 1, page_size: int = 50) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
获取指定页码的融资融券数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 页码
|
||||||
|
page_size: 每页数据量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含融资融券数据的DataFrame
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
"reportName": "RPTA_RZRQ_LSHJ",
|
||||||
|
"columns": "ALL",
|
||||||
|
"source": "WEB",
|
||||||
|
"sortColumns": "dim_date",
|
||||||
|
"sortTypes": "-1",
|
||||||
|
"pageNumber": page,
|
||||||
|
"pageSize": page_size
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"开始获取第 {page} 页数据")
|
||||||
|
|
||||||
|
response = requests.get(self.base_url, params=params, headers=self.headers)
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"获取第 {page} 页数据失败: HTTP {response.status_code}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
if not data.get("success"):
|
||||||
|
logger.error(f"获取数据失败: {data.get('message', '未知错误')}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
# 提取数据列表
|
||||||
|
items = data.get("result", {}).get("data", [])
|
||||||
|
if not items:
|
||||||
|
logger.warning(f"第 {page} 页未找到有效数据")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
# 转换为DataFrame
|
||||||
|
df = pd.DataFrame(items)
|
||||||
|
|
||||||
|
# 重命名列
|
||||||
|
column_mapping = {
|
||||||
|
"DIM_DATE": "trade_date",
|
||||||
|
"NEW": "index_value",
|
||||||
|
"ZDF": "change_percent",
|
||||||
|
"LTSZ": "float_market_value",
|
||||||
|
"ZDF3D": "change_percent_3d",
|
||||||
|
"ZDF5D": "change_percent_5d",
|
||||||
|
"ZDF10D": "change_percent_10d",
|
||||||
|
"RZYE": "financing_balance",
|
||||||
|
"RZYEZB": "financing_balance_ratio",
|
||||||
|
"RZMRE": "financing_buy_amount",
|
||||||
|
"RZMRE3D": "financing_buy_amount_3d",
|
||||||
|
"RZMRE5D": "financing_buy_amount_5d",
|
||||||
|
"RZMRE10D": "financing_buy_amount_10d",
|
||||||
|
"RZCHE": "financing_repay_amount",
|
||||||
|
"RZCHE3D": "financing_repay_amount_3d",
|
||||||
|
"RZCHE5D": "financing_repay_amount_5d",
|
||||||
|
"RZCHE10D": "financing_repay_amount_10d",
|
||||||
|
"RZJME": "financing_net_amount",
|
||||||
|
"RZJME3D": "financing_net_amount_3d",
|
||||||
|
"RZJME5D": "financing_net_amount_5d",
|
||||||
|
"RZJME10D": "financing_net_amount_10d",
|
||||||
|
"RQYE": "securities_balance",
|
||||||
|
"RQYL": "securities_volume",
|
||||||
|
"RQCHL": "securities_repay_volume",
|
||||||
|
"RQCHL3D": "securities_repay_volume_3d",
|
||||||
|
"RQCHL5D": "securities_repay_volume_5d",
|
||||||
|
"RQCHL10D": "securities_repay_volume_10d",
|
||||||
|
"RQMCL": "securities_sell_volume",
|
||||||
|
"RQMCL3D": "securities_sell_volume_3d",
|
||||||
|
"RQMCL5D": "securities_sell_volume_5d",
|
||||||
|
"RQMCL10D": "securities_sell_volume_10d",
|
||||||
|
"RQJMG": "securities_net_volume",
|
||||||
|
"RQJMG3D": "securities_net_volume_3d",
|
||||||
|
"RQJMG5D": "securities_net_volume_5d",
|
||||||
|
"RQJMG10D": "securities_net_volume_10d",
|
||||||
|
"RZRQYE": "total_rzrq_balance",
|
||||||
|
"RZRQYECZ": "total_rzrq_balance_cz"
|
||||||
|
}
|
||||||
|
|
||||||
|
df = df.rename(columns=column_mapping)
|
||||||
|
|
||||||
|
# 转换日期格式
|
||||||
|
df['trade_date'] = df['trade_date'].apply(self._parse_date)
|
||||||
|
|
||||||
|
logger.info(f"第 {page} 页数据获取成功,包含 {len(df)} 条记录")
|
||||||
|
return df
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取第 {page} 页数据失败: {e}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def fetch_all_data(self, max_pages: int = 368) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
获取所有页的融资融券数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_pages: 最大页数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含所有融资融券数据的DataFrame
|
||||||
|
"""
|
||||||
|
all_data = []
|
||||||
|
|
||||||
|
for page in range(1, max_pages + 1):
|
||||||
|
page_data = self.fetch_data(page)
|
||||||
|
if page_data.empty:
|
||||||
|
logger.info(f"第 {page} 页数据为空,停止采集")
|
||||||
|
break
|
||||||
|
all_data.append(page_data)
|
||||||
|
# 添加延迟,避免请求过于频繁
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if all_data:
|
||||||
|
combined_df = pd.concat(all_data, ignore_index=True)
|
||||||
|
logger.info(f"数据采集完成,共采集 {len(combined_df)} 条记录")
|
||||||
|
return combined_df
|
||||||
|
else:
|
||||||
|
logger.warning("未获取到任何有效数据")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def save_to_database(self, data: pd.DataFrame) -> bool:
|
||||||
|
"""
|
||||||
|
将数据保存到数据库
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: 要保存的数据DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功保存数据
|
||||||
|
"""
|
||||||
|
if data.empty:
|
||||||
|
logger.warning("没有数据需要保存")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 确保数据表存在
|
||||||
|
if not self._ensure_table_exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 将nan值转换为None(在SQL中会变成NULL)
|
||||||
|
data = data.replace({pd.NA: None, pd.NaT: None})
|
||||||
|
data = data.where(pd.notnull(data), None)
|
||||||
|
|
||||||
|
# 添加数据或更新已有数据
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
for _, row in data.iterrows():
|
||||||
|
# 将Series转换为dict,并处理nan值
|
||||||
|
row_dict = {k: (None if pd.isna(v) else v) for k, v in row.items()}
|
||||||
|
|
||||||
|
# 检查该日期的数据是否已存在
|
||||||
|
check_query = text("""
|
||||||
|
SELECT COUNT(*) FROM eastmoney_rzrq_data WHERE trade_date = :trade_date
|
||||||
|
""")
|
||||||
|
result = conn.execute(check_query, {"trade_date": row_dict['trade_date']}).scalar()
|
||||||
|
|
||||||
|
if result > 0: # 数据已存在,执行更新
|
||||||
|
update_query = text("""
|
||||||
|
UPDATE eastmoney_rzrq_data SET
|
||||||
|
index_value = :index_value,
|
||||||
|
change_percent = :change_percent,
|
||||||
|
float_market_value = :float_market_value,
|
||||||
|
change_percent_3d = :change_percent_3d,
|
||||||
|
change_percent_5d = :change_percent_5d,
|
||||||
|
change_percent_10d = :change_percent_10d,
|
||||||
|
financing_balance = :financing_balance,
|
||||||
|
financing_balance_ratio = :financing_balance_ratio,
|
||||||
|
financing_buy_amount = :financing_buy_amount,
|
||||||
|
financing_buy_amount_3d = :financing_buy_amount_3d,
|
||||||
|
financing_buy_amount_5d = :financing_buy_amount_5d,
|
||||||
|
financing_buy_amount_10d = :financing_buy_amount_10d,
|
||||||
|
financing_repay_amount = :financing_repay_amount,
|
||||||
|
financing_repay_amount_3d = :financing_repay_amount_3d,
|
||||||
|
financing_repay_amount_5d = :financing_repay_amount_5d,
|
||||||
|
financing_repay_amount_10d = :financing_repay_amount_10d,
|
||||||
|
financing_net_amount = :financing_net_amount,
|
||||||
|
financing_net_amount_3d = :financing_net_amount_3d,
|
||||||
|
financing_net_amount_5d = :financing_net_amount_5d,
|
||||||
|
financing_net_amount_10d = :financing_net_amount_10d,
|
||||||
|
securities_balance = :securities_balance,
|
||||||
|
securities_volume = :securities_volume,
|
||||||
|
securities_repay_volume = :securities_repay_volume,
|
||||||
|
securities_repay_volume_3d = :securities_repay_volume_3d,
|
||||||
|
securities_repay_volume_5d = :securities_repay_volume_5d,
|
||||||
|
securities_repay_volume_10d = :securities_repay_volume_10d,
|
||||||
|
securities_sell_volume = :securities_sell_volume,
|
||||||
|
securities_sell_volume_3d = :securities_sell_volume_3d,
|
||||||
|
securities_sell_volume_5d = :securities_sell_volume_5d,
|
||||||
|
securities_sell_volume_10d = :securities_sell_volume_10d,
|
||||||
|
securities_net_volume = :securities_net_volume,
|
||||||
|
securities_net_volume_3d = :securities_net_volume_3d,
|
||||||
|
securities_net_volume_5d = :securities_net_volume_5d,
|
||||||
|
securities_net_volume_10d = :securities_net_volume_10d,
|
||||||
|
total_rzrq_balance = :total_rzrq_balance,
|
||||||
|
total_rzrq_balance_cz = :total_rzrq_balance_cz
|
||||||
|
WHERE trade_date = :trade_date
|
||||||
|
""")
|
||||||
|
conn.execute(update_query, row_dict)
|
||||||
|
updated_count += 1
|
||||||
|
else: # 数据不存在,执行插入
|
||||||
|
insert_query = text("""
|
||||||
|
INSERT INTO eastmoney_rzrq_data (
|
||||||
|
trade_date, index_value, change_percent, float_market_value,
|
||||||
|
change_percent_3d, change_percent_5d, change_percent_10d,
|
||||||
|
financing_balance, financing_balance_ratio,
|
||||||
|
financing_buy_amount, financing_buy_amount_3d, financing_buy_amount_5d, financing_buy_amount_10d,
|
||||||
|
financing_repay_amount, financing_repay_amount_3d, financing_repay_amount_5d, financing_repay_amount_10d,
|
||||||
|
financing_net_amount, financing_net_amount_3d, financing_net_amount_5d, financing_net_amount_10d,
|
||||||
|
securities_balance, securities_volume,
|
||||||
|
securities_repay_volume, securities_repay_volume_3d, securities_repay_volume_5d, securities_repay_volume_10d,
|
||||||
|
securities_sell_volume, securities_sell_volume_3d, securities_sell_volume_5d, securities_sell_volume_10d,
|
||||||
|
securities_net_volume, securities_net_volume_3d, securities_net_volume_5d, securities_net_volume_10d,
|
||||||
|
total_rzrq_balance, total_rzrq_balance_cz
|
||||||
|
) VALUES (
|
||||||
|
:trade_date, :index_value, :change_percent, :float_market_value,
|
||||||
|
:change_percent_3d, :change_percent_5d, :change_percent_10d,
|
||||||
|
:financing_balance, :financing_balance_ratio,
|
||||||
|
:financing_buy_amount, :financing_buy_amount_3d, :financing_buy_amount_5d, :financing_buy_amount_10d,
|
||||||
|
:financing_repay_amount, :financing_repay_amount_3d, :financing_repay_amount_5d, :financing_repay_amount_10d,
|
||||||
|
:financing_net_amount, :financing_net_amount_3d, :financing_net_amount_5d, :financing_net_amount_10d,
|
||||||
|
:securities_balance, :securities_volume,
|
||||||
|
:securities_repay_volume, :securities_repay_volume_3d, :securities_repay_volume_5d, :securities_repay_volume_10d,
|
||||||
|
:securities_sell_volume, :securities_sell_volume_3d, :securities_sell_volume_5d, :securities_sell_volume_10d,
|
||||||
|
:securities_net_volume, :securities_net_volume_3d, :securities_net_volume_5d, :securities_net_volume_10d,
|
||||||
|
:total_rzrq_balance, :total_rzrq_balance_cz
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute(insert_query, row_dict)
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
logger.info(f"数据保存成功:新增 {inserted_count} 条记录,更新 {updated_count} 条记录")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存数据到数据库失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_latest_data(self) -> bool:
|
||||||
|
"""
|
||||||
|
更新最新一天的融资融券数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功更新最新数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("开始更新最新一天的融资融券数据")
|
||||||
|
|
||||||
|
# 获取第一页数据
|
||||||
|
df = self.fetch_data(1)
|
||||||
|
if df.empty:
|
||||||
|
logger.warning("未获取到最新数据")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 只保留第一行(最新一天的数据)
|
||||||
|
latest_data = df.iloc[:1]
|
||||||
|
|
||||||
|
# 保存数据到数据库
|
||||||
|
result = self.save_to_database(latest_data)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info(f"最新数据({latest_data.iloc[0]['trade_date']})更新成功")
|
||||||
|
else:
|
||||||
|
logger.warning("最新数据更新失败")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新最新数据失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def initial_data_collection(self) -> bool:
|
||||||
|
"""
|
||||||
|
首次全量采集融资融券数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功采集所有数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("开始获取最新融资融券数据...")
|
||||||
|
df = collector.fetch_data(page=1)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
# 保存数据到数据库
|
||||||
|
if collector.save_to_database(df):
|
||||||
|
logger.info(f"成功更新最新数据,日期:{df.iloc[0]['trade_date']}")
|
||||||
|
else:
|
||||||
|
logger.error("更新最新数据失败")
|
||||||
|
else:
|
||||||
|
logger.warning("未获取到最新数据")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"首次全量采集失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_chart_data(self, limit_days: int = 30) -> dict:
|
||||||
|
"""
|
||||||
|
获取融资融券数据用于图表展示
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit_days: 获取最近多少天的数据,默认30天
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含图表所需数据的字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"获取最近 {limit_days} 天融资融券数据用于图表展示")
|
||||||
|
|
||||||
|
# 构建SQL查询
|
||||||
|
query = text("""
|
||||||
|
SELECT
|
||||||
|
trade_date,
|
||||||
|
financing_balance,
|
||||||
|
financing_buy_amount,
|
||||||
|
securities_balance,
|
||||||
|
total_rzrq_balance
|
||||||
|
FROM eastmoney_rzrq_data
|
||||||
|
ORDER BY trade_date DESC
|
||||||
|
LIMIT :limit_days
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 执行查询
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
result = conn.execute(query, {"limit_days": limit_days}).fetchall()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
logger.warning("未找到融资融券数据")
|
||||||
|
return {"success": False, "message": "未找到融资融券数据"}
|
||||||
|
|
||||||
|
# 将结果转换为列表并倒序,使日期按升序排列
|
||||||
|
rows = []
|
||||||
|
for row in result:
|
||||||
|
# 将每行结果转换为字典
|
||||||
|
row_dict = {
|
||||||
|
'trade_date': row.trade_date,
|
||||||
|
'financing_balance': row.financing_balance,
|
||||||
|
'financing_buy_amount': row.financing_buy_amount,
|
||||||
|
'securities_balance': row.securities_balance,
|
||||||
|
'total_rzrq_balance': row.total_rzrq_balance
|
||||||
|
}
|
||||||
|
rows.append(row_dict)
|
||||||
|
|
||||||
|
# 反转列表使日期按升序排列
|
||||||
|
rows.reverse()
|
||||||
|
|
||||||
|
# 准备数据
|
||||||
|
dates = []
|
||||||
|
total_rzrq_balance = [] # 融资融券余额合计
|
||||||
|
total_financing_buy = [] # 融资买入额合计
|
||||||
|
total_financing_balance = [] # 融资余额合计
|
||||||
|
financing_repayment = [] # 融资偿还
|
||||||
|
securities_balance = [] # 融券余额
|
||||||
|
|
||||||
|
prev_financing_balance = None # 上一日融资余额
|
||||||
|
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
dates.append(row['trade_date'].strftime('%Y-%m-%d'))
|
||||||
|
# 将金额从元转换为亿元(除以1亿)
|
||||||
|
total_rzrq_balance.append(round(float(row['total_rzrq_balance']) / 100000000, 2))
|
||||||
|
total_financing_buy.append(round(float(row['financing_buy_amount']) / 100000000, 2))
|
||||||
|
total_financing_balance.append(round(float(row['financing_balance']) / 100000000, 2))
|
||||||
|
securities_balance.append(round(float(row['securities_balance']) / 100000000, 2))
|
||||||
|
|
||||||
|
# 计算融资偿还 = 融资买入额 + 前一日融资余额 - 当日融资余额
|
||||||
|
if i > 0 and prev_financing_balance is not None:
|
||||||
|
# 注意:这里不需要再除以1亿,因为前面的数据已经是亿元单位
|
||||||
|
repayment = float(row['financing_buy_amount']) / 100000000 + prev_financing_balance - float(row['financing_balance']) / 100000000
|
||||||
|
financing_repayment.append(round(repayment, 2))
|
||||||
|
else:
|
||||||
|
financing_repayment.append(None) # 第一天无法计算
|
||||||
|
|
||||||
|
prev_financing_balance = float(row['financing_balance']) / 100000000 # 转换为亿元单位
|
||||||
|
|
||||||
|
# 计算市场风险分析指标
|
||||||
|
risk_indicators = self.analyze_market_risk(
|
||||||
|
dates=dates,
|
||||||
|
total_rzrq_balance=total_rzrq_balance,
|
||||||
|
total_financing_balance=total_financing_balance,
|
||||||
|
total_financing_buy=total_financing_buy,
|
||||||
|
securities_balance=securities_balance
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
chart_data = {
|
||||||
|
"success": True,
|
||||||
|
"dates": dates,
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "融资融券余额合计",
|
||||||
|
"data": total_rzrq_balance,
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "融资买入额合计",
|
||||||
|
"data": total_financing_buy,
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "融资余额合计",
|
||||||
|
"data": total_financing_balance,
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "融资偿还",
|
||||||
|
"data": financing_repayment,
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "融券余额",
|
||||||
|
"data": securities_balance,
|
||||||
|
"unit": "亿元"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"risk_indicators": risk_indicators
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加更新时间
|
||||||
|
chart_data["last_update"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
logger.info(f"成功获取 {len(dates)} 天的融资融券图表数据")
|
||||||
|
return chart_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取融资融券图表数据失败: {e}")
|
||||||
|
return {"success": False, "message": f"获取数据失败: {str(e)}"}
|
||||||
|
|
||||||
|
def analyze_market_risk(self, dates, total_rzrq_balance, total_financing_balance, total_financing_buy, securities_balance):
|
||||||
|
"""
|
||||||
|
分析融资融券数据,计算市场风险指标
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dates: 日期列表
|
||||||
|
total_rzrq_balance: 融资融券余额合计列表
|
||||||
|
total_financing_balance: 融资余额合计列表
|
||||||
|
total_financing_buy: 融资买入额合计列表
|
||||||
|
securities_balance: 融券余额列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含风险指标的字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 确保数据长度足够
|
||||||
|
if len(dates) < 5:
|
||||||
|
return {"warning": "数据量不足,无法进行完整分析"}
|
||||||
|
|
||||||
|
risk_indicators = {}
|
||||||
|
|
||||||
|
# 1. 计算融资融券余额趋势 - 近期变化率
|
||||||
|
recent_days = 5 # 分析最近5天的变化
|
||||||
|
if len(total_rzrq_balance) >= recent_days:
|
||||||
|
latest_balance = total_rzrq_balance[-1]
|
||||||
|
prev_balance = total_rzrq_balance[-recent_days]
|
||||||
|
|
||||||
|
if prev_balance > 0:
|
||||||
|
balance_change_rate = (latest_balance - prev_balance) / prev_balance * 100
|
||||||
|
balance_change_direction = "上升" if balance_change_rate > 0 else "下降"
|
||||||
|
|
||||||
|
risk_indicators["recent_balance_change"] = {
|
||||||
|
"rate": round(balance_change_rate, 2),
|
||||||
|
"direction": balance_change_direction,
|
||||||
|
"days": recent_days,
|
||||||
|
"start_date": dates[-recent_days],
|
||||||
|
"end_date": dates[-1],
|
||||||
|
"start_value": round(prev_balance, 2),
|
||||||
|
"end_value": round(latest_balance, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 风险评级 - 融资融券余额变化
|
||||||
|
if balance_change_rate > 10:
|
||||||
|
balance_risk_level = "高"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额快速上升{abs(round(balance_change_rate, 2))}%,市场杠杆水平快速提升,风险较高"
|
||||||
|
elif balance_change_rate > 5:
|
||||||
|
balance_risk_level = "中"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额上升{abs(round(balance_change_rate, 2))}%,市场杠杆水平有所提升"
|
||||||
|
elif balance_change_rate < -10:
|
||||||
|
balance_risk_level = "高"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额快速下降{abs(round(balance_change_rate, 2))}%,市场杠杆水平快速下降,可能伴随抛压"
|
||||||
|
elif balance_change_rate < -5:
|
||||||
|
balance_risk_level = "中"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额下降{abs(round(balance_change_rate, 2))}%,市场杠杆水平有所下降"
|
||||||
|
else:
|
||||||
|
balance_risk_level = "低"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额变化{round(balance_change_rate, 2)}%,市场杠杆水平相对稳定"
|
||||||
|
|
||||||
|
risk_indicators["balance_risk"] = {
|
||||||
|
"level": balance_risk_level,
|
||||||
|
"description": balance_risk_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. 计算融资偿还比例 - 衡量资金偿还压力
|
||||||
|
# 融资偿还比例 = 当日融资偿还 / 当日融资买入
|
||||||
|
recent_days_repay = min(5, len(total_financing_buy) - 1) # 考虑数据长度
|
||||||
|
if recent_days_repay > 0:
|
||||||
|
# 计算近几天的平均偿还买入比
|
||||||
|
repay_buy_ratios = []
|
||||||
|
for i in range(1, recent_days_repay + 1):
|
||||||
|
if total_financing_buy[-i] > 0:
|
||||||
|
# 融资偿还 = 当日融资买入额 + 前一日融资余额 - 当日融资余额
|
||||||
|
if i < len(total_financing_balance) and i+1 < len(total_financing_balance):
|
||||||
|
repayment = total_financing_buy[-i] + total_financing_balance[-(i+1)] - total_financing_balance[-i]
|
||||||
|
ratio = repayment / total_financing_buy[-i]
|
||||||
|
repay_buy_ratios.append(ratio)
|
||||||
|
|
||||||
|
if repay_buy_ratios:
|
||||||
|
avg_repay_buy_ratio = sum(repay_buy_ratios) / len(repay_buy_ratios)
|
||||||
|
|
||||||
|
risk_indicators["repay_buy_ratio"] = {
|
||||||
|
"value": round(avg_repay_buy_ratio, 2),
|
||||||
|
"days": recent_days_repay
|
||||||
|
}
|
||||||
|
|
||||||
|
# 风险评级 - 偿还率
|
||||||
|
if avg_repay_buy_ratio > 1.2:
|
||||||
|
repay_risk_level = "高"
|
||||||
|
repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},偿还明显大于买入,市场存在较强的抛压"
|
||||||
|
elif avg_repay_buy_ratio > 1.05:
|
||||||
|
repay_risk_level = "中"
|
||||||
|
repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},偿还略大于买入,市场抛压增加"
|
||||||
|
elif avg_repay_buy_ratio < 0.8:
|
||||||
|
repay_risk_level = "中"
|
||||||
|
repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},买入明显大于偿还,杠杆快速提升"
|
||||||
|
else:
|
||||||
|
repay_risk_level = "低"
|
||||||
|
repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},买入与偿还较为平衡"
|
||||||
|
|
||||||
|
risk_indicators["repay_risk"] = {
|
||||||
|
"level": repay_risk_level,
|
||||||
|
"description": repay_risk_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. 融券余额变化 - 分析空头力量
|
||||||
|
if len(securities_balance) >= recent_days:
|
||||||
|
latest_securities = securities_balance[-1]
|
||||||
|
prev_securities = securities_balance[-recent_days]
|
||||||
|
|
||||||
|
if prev_securities > 0:
|
||||||
|
securities_change_rate = (latest_securities - prev_securities) / prev_securities * 100
|
||||||
|
securities_change_direction = "上升" if securities_change_rate > 0 else "下降"
|
||||||
|
|
||||||
|
risk_indicators["securities_balance_change"] = {
|
||||||
|
"rate": round(securities_change_rate, 2),
|
||||||
|
"direction": securities_change_direction,
|
||||||
|
"days": recent_days,
|
||||||
|
"start_value": round(prev_securities, 2),
|
||||||
|
"end_value": round(latest_securities, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 风险评级 - 融券余额变化
|
||||||
|
if securities_change_rate > 15:
|
||||||
|
securities_risk_level = "高"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额快速上升{abs(round(securities_change_rate, 2))}%,空头力量显著增强"
|
||||||
|
elif securities_change_rate > 8:
|
||||||
|
securities_risk_level = "中"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额上升{abs(round(securities_change_rate, 2))}%,空头力量有所增强"
|
||||||
|
elif securities_change_rate < -15:
|
||||||
|
securities_risk_level = "低"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额快速下降{abs(round(securities_change_rate, 2))}%,空头力量显著减弱"
|
||||||
|
elif securities_change_rate < -8:
|
||||||
|
securities_risk_level = "低"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额下降{abs(round(securities_change_rate, 2))}%,空头力量有所减弱"
|
||||||
|
else:
|
||||||
|
securities_risk_level = "正常"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额变化{round(securities_change_rate, 2)}%,空头力量相对稳定"
|
||||||
|
|
||||||
|
risk_indicators["securities_risk"] = {
|
||||||
|
"level": securities_risk_level,
|
||||||
|
"description": securities_risk_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. 融资占比 - 融资余额占融资融券余额的比例
|
||||||
|
if total_rzrq_balance[-1] > 0:
|
||||||
|
financing_ratio = total_financing_balance[-1] / total_rzrq_balance[-1]
|
||||||
|
risk_indicators["financing_ratio"] = round(financing_ratio * 100, 2)
|
||||||
|
|
||||||
|
# 融资占比的历史百分位数计算
|
||||||
|
if len(total_rzrq_balance) > 20: # 至少需要20天数据计算有意义的百分位数
|
||||||
|
historical_ratios = []
|
||||||
|
for i in range(len(total_rzrq_balance)):
|
||||||
|
if total_rzrq_balance[i] > 0:
|
||||||
|
ratio = total_financing_balance[i] / total_rzrq_balance[i]
|
||||||
|
historical_ratios.append(ratio)
|
||||||
|
|
||||||
|
if historical_ratios:
|
||||||
|
# 计算融资占比的百分位数
|
||||||
|
sorted_ratios = sorted(historical_ratios)
|
||||||
|
percentile_rank = sorted_ratios.index(financing_ratio) / len(sorted_ratios) * 100 if financing_ratio in sorted_ratios else 0
|
||||||
|
|
||||||
|
# 如果不在列表中,找到最接近的位置
|
||||||
|
if percentile_rank == 0:
|
||||||
|
for i, ratio in enumerate(sorted_ratios):
|
||||||
|
if ratio > financing_ratio:
|
||||||
|
percentile_rank = i / len(sorted_ratios) * 100
|
||||||
|
break
|
||||||
|
if percentile_rank == 0 and financing_ratio > sorted_ratios[-1]:
|
||||||
|
percentile_rank = 100
|
||||||
|
|
||||||
|
risk_indicators["financing_ratio_percentile"] = round(percentile_rank, 2)
|
||||||
|
|
||||||
|
# 5. 综合风险评估
|
||||||
|
risk_levels = []
|
||||||
|
if "balance_risk" in risk_indicators:
|
||||||
|
risk_levels.append(risk_indicators["balance_risk"]["level"])
|
||||||
|
if "repay_risk" in risk_indicators:
|
||||||
|
risk_levels.append(risk_indicators["repay_risk"]["level"])
|
||||||
|
if "securities_risk" in risk_indicators:
|
||||||
|
risk_levels.append(risk_indicators["securities_risk"]["level"])
|
||||||
|
|
||||||
|
# 计算综合风险等级
|
||||||
|
high_count = risk_levels.count("高")
|
||||||
|
medium_count = risk_levels.count("中")
|
||||||
|
|
||||||
|
if high_count >= 2:
|
||||||
|
overall_risk = "高"
|
||||||
|
overall_desc = "多个指标显示市场风险较高,融资融券数据反映市场杠杆或波动风险增加"
|
||||||
|
elif high_count == 1 or medium_count >= 2:
|
||||||
|
overall_risk = "中"
|
||||||
|
overall_desc = "部分指标显示市场风险增加,建议关注融资融券数据变化"
|
||||||
|
else:
|
||||||
|
overall_risk = "低"
|
||||||
|
overall_desc = "融资融券数据显示市场风险较低,杠杆水平相对稳定"
|
||||||
|
|
||||||
|
risk_indicators["overall_risk"] = {
|
||||||
|
"level": overall_risk,
|
||||||
|
"description": overall_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
return risk_indicators
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"分析市场风险指标失败: {e}")
|
||||||
|
return {"error": f"分析失败: {str(e)}"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_create_table_sql() -> str:
|
||||||
|
"""
|
||||||
|
获取创建东方财富融资融券数据表的SQL语句
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
创建表的SQL语句
|
||||||
|
"""
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS eastmoney_rzrq_data (
|
||||||
|
trade_date DATE PRIMARY KEY,
|
||||||
|
index_value DECIMAL(10,4) COMMENT '指数',
|
||||||
|
change_percent DECIMAL(10,4) COMMENT '涨跌幅',
|
||||||
|
float_market_value DECIMAL(20,2) COMMENT '流通市值',
|
||||||
|
change_percent_3d DECIMAL(10,4) COMMENT '3日涨跌幅',
|
||||||
|
change_percent_5d DECIMAL(10,4) COMMENT '5日涨跌幅',
|
||||||
|
change_percent_10d DECIMAL(10,4) COMMENT '10日涨跌幅',
|
||||||
|
financing_balance DECIMAL(20,2) COMMENT '融资余额',
|
||||||
|
financing_balance_ratio DECIMAL(10,4) COMMENT '融资余额占比',
|
||||||
|
financing_buy_amount DECIMAL(20,2) COMMENT '融资买入额',
|
||||||
|
financing_buy_amount_3d DECIMAL(20,2) COMMENT '3日融资买入额',
|
||||||
|
financing_buy_amount_5d DECIMAL(20,2) COMMENT '5日融资买入额',
|
||||||
|
financing_buy_amount_10d DECIMAL(20,2) COMMENT '10日融资买入额',
|
||||||
|
financing_repay_amount DECIMAL(20,2) COMMENT '融资偿还额',
|
||||||
|
financing_repay_amount_3d DECIMAL(20,2) COMMENT '3日融资偿还额',
|
||||||
|
financing_repay_amount_5d DECIMAL(20,2) COMMENT '5日融资偿还额',
|
||||||
|
financing_repay_amount_10d DECIMAL(20,2) COMMENT '10日融资偿还额',
|
||||||
|
financing_net_amount DECIMAL(20,2) COMMENT '融资净额',
|
||||||
|
financing_net_amount_3d DECIMAL(20,2) COMMENT '3日融资净额',
|
||||||
|
financing_net_amount_5d DECIMAL(20,2) COMMENT '5日融资净额',
|
||||||
|
financing_net_amount_10d DECIMAL(20,2) COMMENT '10日融资净额',
|
||||||
|
securities_balance DECIMAL(20,2) COMMENT '融券余额',
|
||||||
|
securities_volume DECIMAL(20,2) COMMENT '融券余量',
|
||||||
|
securities_repay_volume DECIMAL(20,2) COMMENT '融券偿还量',
|
||||||
|
securities_repay_volume_3d DECIMAL(20,2) COMMENT '3日融券偿还量',
|
||||||
|
securities_repay_volume_5d DECIMAL(20,2) COMMENT '5日融券偿还量',
|
||||||
|
securities_repay_volume_10d DECIMAL(20,2) COMMENT '10日融券偿还量',
|
||||||
|
securities_sell_volume DECIMAL(20,2) COMMENT '融券卖出量',
|
||||||
|
securities_sell_volume_3d DECIMAL(20,2) COMMENT '3日融券卖出量',
|
||||||
|
securities_sell_volume_5d DECIMAL(20,2) COMMENT '5日融券卖出量',
|
||||||
|
securities_sell_volume_10d DECIMAL(20,2) COMMENT '10日融券卖出量',
|
||||||
|
securities_net_volume DECIMAL(20,2) COMMENT '融券净量',
|
||||||
|
securities_net_volume_3d DECIMAL(20,2) COMMENT '3日融券净量',
|
||||||
|
securities_net_volume_5d DECIMAL(20,2) COMMENT '5日融券净量',
|
||||||
|
securities_net_volume_10d DECIMAL(20,2) COMMENT '10日融券净量',
|
||||||
|
total_rzrq_balance DECIMAL(20,2) COMMENT '融资融券余额',
|
||||||
|
total_rzrq_balance_cz DECIMAL(20,2) COMMENT '融资融券余额差值',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='东方财富融资融券数据表';
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# 示例使用方式
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建东方财富融资融券数据采集器
|
||||||
|
collector = EastmoneyRzrqCollector()
|
||||||
|
|
||||||
|
# 获取最新一页数据
|
||||||
|
logger.info("开始获取最新融资融券数据...")
|
||||||
|
df = collector.fetch_data(page=1)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
# 保存数据到数据库
|
||||||
|
if collector.save_to_database(df):
|
||||||
|
logger.info(f"成功更新最新数据,日期:{df.iloc[0]['trade_date']}")
|
||||||
|
else:
|
||||||
|
logger.error("更新最新数据失败")
|
||||||
|
else:
|
||||||
|
logger.warning("未获取到最新数据")
|
||||||
|
|
||||||
|
# 输出创建表的SQL语句
|
||||||
|
# print("创建表的SQL语句:")
|
||||||
|
# print(get_create_table_sql())
|
||||||
|
#
|
||||||
|
# # 首次全量采集数据
|
||||||
|
# print("\n开始首次全量采集数据...")
|
||||||
|
# collector.initial_data_collection()
|
|
@ -0,0 +1,242 @@
|
||||||
|
"""
|
||||||
|
恐贪指数(Fear & Greed Index)数据管理模块
|
||||||
|
|
||||||
|
提供恐贪指数的数据库操作功能,包括:
|
||||||
|
1. 创建恐贪指数数据表
|
||||||
|
2. 新增恐贪指数数据
|
||||||
|
3. 查询恐贪指数数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from .config import DB_URL, LOG_FILE
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("fear_greed_index")
|
||||||
|
|
||||||
|
|
||||||
|
class FearGreedIndexManager:
|
||||||
|
"""恐贪指数数据管理器类"""
|
||||||
|
|
||||||
|
def __init__(self, db_url: str = DB_URL):
|
||||||
|
"""
|
||||||
|
初始化恐贪指数数据管理器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_url: 数据库连接URL
|
||||||
|
"""
|
||||||
|
self.engine = create_engine(
|
||||||
|
db_url,
|
||||||
|
pool_size=5,
|
||||||
|
max_overflow=10,
|
||||||
|
pool_recycle=3600
|
||||||
|
)
|
||||||
|
# 确保数据表存在
|
||||||
|
self._ensure_table_exists()
|
||||||
|
logger.info("恐贪指数数据管理器初始化完成")
|
||||||
|
|
||||||
|
def _ensure_table_exists(self) -> bool:
|
||||||
|
"""
|
||||||
|
确保恐贪指数数据表存在,如果不存在则创建
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功确保表存在
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 创建恐贪指数表的SQL语句
|
||||||
|
create_table_sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS `fear_greed_index` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`index_value` decimal(5,2) NOT NULL COMMENT '恐贪指数值(0-100)',
|
||||||
|
`trading_date` date NOT NULL COMMENT '交易日期',
|
||||||
|
`update_time` datetime NOT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_trading_date` (`trading_date`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场恐贪指数数据';
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 执行建表语句
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
conn.execute(text(create_table_sql))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
logger.info("恐贪指数数据表创建成功或已存在")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"确保恐贪指数数据表存在失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_index_data(self, index_value: float, trading_date: str) -> bool:
|
||||||
|
"""
|
||||||
|
添加恐贪指数数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index_value: 恐贪指数值,范围0-100,保留两位小数
|
||||||
|
trading_date: 交易日期,格式为 YYYY-MM-DD
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功添加数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证指数值范围
|
||||||
|
if not 0 <= index_value <= 100:
|
||||||
|
logger.error(f"恐贪指数值 {index_value} 超出范围(0-100)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 验证日期格式
|
||||||
|
try:
|
||||||
|
formatted_date = datetime.datetime.strptime(trading_date, "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"无效的日期格式: {trading_date},应为YYYY-MM-DD格式")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 当前时间作为更新时间
|
||||||
|
update_time = datetime.datetime.now()
|
||||||
|
|
||||||
|
# 构建插入SQL
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO fear_greed_index (index_value, trading_date, update_time)
|
||||||
|
VALUES (:index_value, :trading_date, :update_time)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
index_value = :index_value,
|
||||||
|
update_time = :update_time
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 执行插入操作
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
text(insert_sql),
|
||||||
|
{
|
||||||
|
"index_value": round(index_value, 2),
|
||||||
|
"trading_date": trading_date,
|
||||||
|
"update_time": update_time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
logger.info(f"恐贪指数数据添加成功: 日期={trading_date}, 值={index_value}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"添加恐贪指数数据失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_index_data(self, start_date: str = None, end_date: str = None, limit: int = 730) -> Dict:
|
||||||
|
"""
|
||||||
|
获取恐贪指数数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start_date: 开始日期,格式为YYYY-MM-DD
|
||||||
|
end_date: 结束日期,格式为YYYY-MM-DD
|
||||||
|
limit: 限制返回的记录数量,默认为730条(约两年的交易日数据)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含恐贪指数数据的字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 构建查询条件
|
||||||
|
conditions = []
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
conditions.append("trading_date >= :start_date")
|
||||||
|
params["start_date"] = start_date
|
||||||
|
|
||||||
|
if end_date:
|
||||||
|
conditions.append("trading_date <= :end_date")
|
||||||
|
params["end_date"] = end_date
|
||||||
|
|
||||||
|
# 构建查询SQL
|
||||||
|
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
||||||
|
query = f"""
|
||||||
|
SELECT id, index_value, trading_date, update_time
|
||||||
|
FROM fear_greed_index
|
||||||
|
WHERE {where_clause}
|
||||||
|
ORDER BY trading_date DESC
|
||||||
|
LIMIT :limit
|
||||||
|
"""
|
||||||
|
|
||||||
|
params["limit"] = limit
|
||||||
|
|
||||||
|
# 执行查询
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
result = conn.execute(text(query), params)
|
||||||
|
rows = result.fetchall()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
logger.warning("未找到恐贪指数数据")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "未找到数据"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 处理查询结果
|
||||||
|
data = []
|
||||||
|
for row in rows:
|
||||||
|
data.append({
|
||||||
|
"id": row[0],
|
||||||
|
"index_value": float(row[1]),
|
||||||
|
"trading_date": row[2].strftime("%Y-%m-%d"),
|
||||||
|
"update_time": row[3].strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
})
|
||||||
|
|
||||||
|
# 按日期升序排序,方便生成图表
|
||||||
|
data.reverse()
|
||||||
|
|
||||||
|
# 提取日期和指数值列表
|
||||||
|
dates = [item["trading_date"] for item in data]
|
||||||
|
values = [item["index_value"] for item in data]
|
||||||
|
|
||||||
|
# 最新数据是最大日期的数据
|
||||||
|
data_by_date = sorted(data, key=lambda x: x["trading_date"], reverse=True)
|
||||||
|
latest = data_by_date[0] if data_by_date else None
|
||||||
|
|
||||||
|
# 计算恐贪指数状态
|
||||||
|
def get_index_status(value):
|
||||||
|
if 0 <= value < 25:
|
||||||
|
return "极度恐慌"
|
||||||
|
elif 25 <= value < 40:
|
||||||
|
return "恐慌"
|
||||||
|
elif 40 <= value < 50:
|
||||||
|
return "偏向恐慌"
|
||||||
|
elif 50 <= value < 60:
|
||||||
|
return "中性"
|
||||||
|
elif 60 <= value < 75:
|
||||||
|
return "偏向贪婪"
|
||||||
|
elif 75 <= value < 90:
|
||||||
|
return "贪婪"
|
||||||
|
else:
|
||||||
|
return "极度贪婪"
|
||||||
|
|
||||||
|
# 计算最新状态
|
||||||
|
latest_status = get_index_status(latest["index_value"]) if latest else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"dates": dates,
|
||||||
|
"values": values,
|
||||||
|
"data": data,
|
||||||
|
"latest": latest,
|
||||||
|
"latest_status": latest_status,
|
||||||
|
"update_time": latest["update_time"] if latest else datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取恐贪指数数据失败: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"获取数据失败: {str(e)}"
|
||||||
|
}
|
|
@ -5,8 +5,6 @@ import requests
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import pandas as pd
|
|
||||||
import time
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -146,7 +144,7 @@ class HSGTMonitor:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"请求{flow_type}资金数据: start={start_timestamp}, end={end_timestamp}, index_id={index_id}")
|
# logger.info(f"请求{flow_type}资金数据: start={start_timestamp}, end={end_timestamp}, index_id={index_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 发送请求
|
# 发送请求
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from src.valuation_analysis.config import DB_URL
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class IndexAnalyzer:
|
||||||
|
"""指数数据分析工具类"""
|
||||||
|
|
||||||
|
def __init__(self, db_url=None):
|
||||||
|
# 初始化数据库连接
|
||||||
|
self.db_url = db_url or DB_URL
|
||||||
|
self.engine = create_engine(self.db_url)
|
||||||
|
|
||||||
|
def get_indices_list(self):
|
||||||
|
"""
|
||||||
|
获取可用指数列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 包含指数信息的列表 [{"id": id, "name": name, "code": code}, ...]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
query = text("""
|
||||||
|
SELECT id, gp_name as name, gp_code as code
|
||||||
|
FROM gp_code_zs
|
||||||
|
ORDER BY gp_name
|
||||||
|
""")
|
||||||
|
result = conn.execute(query).fetchall()
|
||||||
|
|
||||||
|
indices = []
|
||||||
|
for row in result:
|
||||||
|
indices.append({
|
||||||
|
"id": row[0],
|
||||||
|
"name": row[1],
|
||||||
|
"code": row[2]
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"获取到 {len(indices)} 个指数")
|
||||||
|
return indices
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取指数列表失败: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_index_data(self, index_code, start_date=None, end_date=None):
|
||||||
|
"""
|
||||||
|
获取指数历史数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index_code: 指数代码
|
||||||
|
start_date: 开始日期 (可选,默认为1年前)
|
||||||
|
end_date: 结束日期 (可选,默认为今天)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含指数数据的字典 {"code": code, "dates": [...], "values": [...]}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 处理日期参数
|
||||||
|
if end_date is None:
|
||||||
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
if start_date is None:
|
||||||
|
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
query = text("""
|
||||||
|
SELECT timestamp, close
|
||||||
|
FROM gp_day_data
|
||||||
|
WHERE symbol = :symbol
|
||||||
|
AND timestamp BETWEEN :start_date AND :end_date
|
||||||
|
ORDER BY timestamp
|
||||||
|
""")
|
||||||
|
|
||||||
|
result = conn.execute(query, {
|
||||||
|
"symbol": index_code,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date
|
||||||
|
}).fetchall()
|
||||||
|
|
||||||
|
dates = []
|
||||||
|
values = []
|
||||||
|
|
||||||
|
for row in result:
|
||||||
|
dates.append(row[0].strftime('%Y-%m-%d'))
|
||||||
|
# close可能是字符串类型,转换为浮点数
|
||||||
|
values.append(float(row[1]) if row[1] else None)
|
||||||
|
|
||||||
|
logger.info(f"获取指数 {index_code} 数据: {len(dates)} 条记录")
|
||||||
|
return {
|
||||||
|
"code": index_code,
|
||||||
|
"dates": dates,
|
||||||
|
"values": values
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取指数 {index_code} 数据失败: {str(e)}")
|
||||||
|
return {
|
||||||
|
"code": index_code,
|
||||||
|
"dates": [],
|
||||||
|
"values": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# 测试代码
|
||||||
|
if __name__ == "__main__":
|
||||||
|
analyzer = IndexAnalyzer()
|
||||||
|
|
||||||
|
# 测试获取指数列表
|
||||||
|
indices = analyzer.get_indices_list()
|
||||||
|
print(f"指数列表: {indices[:5]}...")
|
||||||
|
|
||||||
|
# 测试获取指数数据
|
||||||
|
if indices:
|
||||||
|
# 测试第一个指数的数据
|
||||||
|
first_index = indices[0]
|
||||||
|
index_data = analyzer.get_index_data(first_index['code'])
|
||||||
|
print(f"指数 {first_index['name']} 数据:")
|
||||||
|
print(f"日期数量: {len(index_data['dates'])}")
|
||||||
|
if index_data['dates']:
|
||||||
|
print(f"第一个日期: {index_data['dates'][0]}, 值: {index_data['values'][0]}")
|
|
@ -85,6 +85,32 @@ class IndustryAnalyzer:
|
||||||
logger.error(f"获取行业列表失败: {e}")
|
logger.error(f"获取行业列表失败: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_concept_list(self) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
获取所有概念板块列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
概念板块列表,每个概念板块为一个字典,包含code和name
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
query = text("""
|
||||||
|
SELECT DISTINCT bk_code, bk_name
|
||||||
|
FROM gp_gnbk
|
||||||
|
ORDER BY bk_name
|
||||||
|
""")
|
||||||
|
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
result = conn.execute(query).fetchall()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return [{"code": str(row[0]), "name": row[1]} for row in result]
|
||||||
|
else:
|
||||||
|
logger.warning("未找到概念板块数据")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取概念板块列表失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def get_industry_stocks(self, industry_name: str) -> List[str]:
|
def get_industry_stocks(self, industry_name: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
获取指定行业的所有股票代码
|
获取指定行业的所有股票代码
|
||||||
|
|
|
@ -0,0 +1,783 @@
|
||||||
|
"""
|
||||||
|
融资融券数据采集模块-采集太麻烦了,已被废弃,用东方财富的非常好用
|
||||||
|
python -m src.valuation_analysis.cli rzrq --action init --output-sql
|
||||||
|
提供从同花顺网站采集融资融券数据并存储到数据库的功能
|
||||||
|
功能包括:
|
||||||
|
1. 采集融资融券数据
|
||||||
|
2. 存储数据到数据库
|
||||||
|
3. 定时自动更新数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from sqlalchemy import create_engine, text
|
||||||
|
|
||||||
|
from .config import DB_URL, LOG_FILE
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("rzrq_collector")
|
||||||
|
|
||||||
|
|
||||||
|
class RzrqCollector:
|
||||||
|
"""融资融券数据采集器类"""
|
||||||
|
|
||||||
|
def __init__(self, db_url: str = DB_URL):
|
||||||
|
"""
|
||||||
|
初始化融资融券数据采集器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_url: 数据库连接URL
|
||||||
|
"""
|
||||||
|
self.engine = create_engine(
|
||||||
|
db_url,
|
||||||
|
pool_size=5,
|
||||||
|
max_overflow=10,
|
||||||
|
pool_recycle=3600
|
||||||
|
)
|
||||||
|
self.base_url = "https://data.10jqka.com.cn/market/rzrq/board/getRzrqPage/page/{}/ajax/1/"
|
||||||
|
self.headers = {
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||||
|
"Cache-Control": "max-age=0",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Cookie": "Hm_lvt_722143063e4892925903024537075d0d=1746513000; HMACCOUNT=8B64A2E3C307C8C0; Hm_lvt_929f8b362150b1f77b477230541dbbc2=1746513000; Hm_lvt_78c58f01938e4d85eaf619eae71b4ed1=1744946910,1746513000; Hm_lvt_60bad21af9c824a4a0530d5dbf4357ca=1746513010; Hm_lvt_f79b64788a4e377c608617fba4c736e2=1746513010; Hm_lpvt_722143063e4892925903024537075d0d=1746513010; Hm_lpvt_929f8b362150b1f77b477230541dbbc2=1746513010; Hm_lpvt_60bad21af9c824a4a0530d5dbf4357ca=1747277468; Hm_lpvt_78c58f01938e4d85eaf619eae71b4ed1=1747277468; Hm_lpvt_f79b64788a4e377c608617fba4c736e2=1747277468; v=AyrTmSqbNs9Y6LqjfoQNiqCwe5vJm6-AoB8imrTj1n0I58QFnCv-BXCvlqGH",
|
||||||
|
"Host": "data.10jqka.com.cn",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "none",
|
||||||
|
"Sec-Fetch-User": "?1",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||||
|
"sec-ch-ua": "\"Chromium\";v=\"134\", \"Not:A-Brand\";v=\"24\", \"Google Chrome\";v=\"134\"",
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": "\"Windows\""
|
||||||
|
}
|
||||||
|
logger.info("融资融券数据采集器初始化完成")
|
||||||
|
|
||||||
|
def _parse_date(self, date_str: str) -> datetime.date:
|
||||||
|
"""将日期字符串解析为日期对象"""
|
||||||
|
if not date_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"日期解析失败: {date_str}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_amount(self, amount_str: str) -> float:
|
||||||
|
"""将金额字符串解析为浮点数"""
|
||||||
|
if not amount_str:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
# 去除逗号和其他非数字字符(除了小数点)
|
||||||
|
clean_str = re.sub(r'[^\d.]', '', amount_str)
|
||||||
|
return float(clean_str)
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"金额解析失败: {amount_str}")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _extract_table_data(self, html_content: str) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
从HTML内容中提取表格数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
html_content: HTML内容
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含融资融券数据的DataFrame
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
soup = BeautifulSoup(html_content, 'html.parser')
|
||||||
|
table = soup.find('table', class_='m-table')
|
||||||
|
|
||||||
|
if not table:
|
||||||
|
logger.error("未找到数据表格")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
rows = table.find_all('tr')
|
||||||
|
data = []
|
||||||
|
|
||||||
|
# 跳过表头行(前两行)
|
||||||
|
for row in rows[2:]: # 从第三行开始是数据行
|
||||||
|
cells = row.find_all('td')
|
||||||
|
if len(cells) >= 17: # 确保行中有足够的单元格
|
||||||
|
row_data = {
|
||||||
|
'trade_date': self._parse_date(cells[0].text.strip()),
|
||||||
|
'sh_financing_balance': self._parse_amount(cells[1].text.strip()),
|
||||||
|
'sz_financing_balance': self._parse_amount(cells[2].text.strip()),
|
||||||
|
'bj_financing_balance': self._parse_amount(cells[3].text.strip()),
|
||||||
|
'total_financing_balance': self._parse_amount(cells[4].text.strip()),
|
||||||
|
'sh_financing_buy': self._parse_amount(cells[5].text.strip()),
|
||||||
|
'sz_financing_buy': self._parse_amount(cells[6].text.strip()),
|
||||||
|
'bj_financing_buy': self._parse_amount(cells[7].text.strip()),
|
||||||
|
'total_financing_buy': self._parse_amount(cells[8].text.strip()),
|
||||||
|
'sh_securities_balance': self._parse_amount(cells[9].text.strip()),
|
||||||
|
'sz_securities_balance': self._parse_amount(cells[10].text.strip()),
|
||||||
|
'bj_securities_balance': self._parse_amount(cells[11].text.strip()),
|
||||||
|
'total_securities_balance': self._parse_amount(cells[12].text.strip()),
|
||||||
|
'sh_rzrq_balance': self._parse_amount(cells[13].text.strip()),
|
||||||
|
'sz_rzrq_balance': self._parse_amount(cells[14].text.strip()),
|
||||||
|
'bj_rzrq_balance': self._parse_amount(cells[15].text.strip()),
|
||||||
|
'total_rzrq_balance': self._parse_amount(cells[16].text.strip()),
|
||||||
|
}
|
||||||
|
data.append(row_data)
|
||||||
|
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
return df
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"提取表格数据失败: {e}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def fetch_page_data(self, page: int = 1) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
获取指定页码的融资融券数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 页码
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含该页融资融券数据的DataFrame
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = self.base_url.format(page)
|
||||||
|
logger.info(f"开始获取第 {page} 页数据: {url}")
|
||||||
|
|
||||||
|
response = requests.get(url, headers=self.headers)
|
||||||
|
print(response.text)
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"获取第 {page} 页数据失败: HTTP {response.status_code}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
df = self._extract_table_data(response.text)
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
logger.warning(f"第 {page} 页未找到有效数据")
|
||||||
|
else:
|
||||||
|
logger.info(f"第 {page} 页数据获取成功,包含 {len(df)} 条记录")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取第 {page} 页数据失败: {e}")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def fetch_all_data(self, pages: int = 4) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
获取所有页的融资融券数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pages: 要获取的总页数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含所有融资融券数据的DataFrame
|
||||||
|
"""
|
||||||
|
all_data = []
|
||||||
|
|
||||||
|
for page in range(1, pages + 1):
|
||||||
|
page_data = self.fetch_page_data(page)
|
||||||
|
if not page_data.empty:
|
||||||
|
all_data.append(page_data)
|
||||||
|
# 添加延迟,避免请求过于频繁
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
if all_data:
|
||||||
|
combined_df = pd.concat(all_data, ignore_index=True)
|
||||||
|
logger.info(f"所有页数据获取完成,共 {len(combined_df)} 条记录")
|
||||||
|
return combined_df
|
||||||
|
else:
|
||||||
|
logger.warning("未获取到任何有效数据")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
def _ensure_table_exists(self) -> bool:
|
||||||
|
"""
|
||||||
|
确保数据表存在,如果不存在则创建
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功确保表存在
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
create_table_query = text("""
|
||||||
|
CREATE TABLE IF NOT EXISTS rzrq_data (
|
||||||
|
trade_date DATE PRIMARY KEY,
|
||||||
|
sh_financing_balance DECIMAL(12,2) COMMENT '上海融资余额(亿元)',
|
||||||
|
sz_financing_balance DECIMAL(12,2) COMMENT '深圳融资余额(亿元)',
|
||||||
|
bj_financing_balance DECIMAL(12,2) COMMENT '北京融资余额(亿元)',
|
||||||
|
total_financing_balance DECIMAL(12,2) COMMENT '融资余额合计(亿元)',
|
||||||
|
sh_financing_buy DECIMAL(12,2) COMMENT '上海融资买入额(亿元)',
|
||||||
|
sz_financing_buy DECIMAL(12,2) COMMENT '深圳融资买入额(亿元)',
|
||||||
|
bj_financing_buy DECIMAL(12,2) COMMENT '北京融资买入额(亿元)',
|
||||||
|
total_financing_buy DECIMAL(12,2) COMMENT '融资买入额合计(亿元)',
|
||||||
|
sh_securities_balance DECIMAL(12,2) COMMENT '上海融券余量余额(亿元)',
|
||||||
|
sz_securities_balance DECIMAL(12,2) COMMENT '深圳融券余量余额(亿元)',
|
||||||
|
bj_securities_balance DECIMAL(12,2) COMMENT '北京融券余量余额(亿元)',
|
||||||
|
total_securities_balance DECIMAL(12,2) COMMENT '融券余量余额合计(亿元)',
|
||||||
|
sh_rzrq_balance DECIMAL(12,2) COMMENT '上海融资融券余额(亿元)',
|
||||||
|
sz_rzrq_balance DECIMAL(12,2) COMMENT '深圳融资融券余额(亿元)',
|
||||||
|
bj_rzrq_balance DECIMAL(12,2) COMMENT '北京融资融券余额(亿元)',
|
||||||
|
total_rzrq_balance DECIMAL(12,2) COMMENT '融资融券余额合计(亿元)',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='融资融券数据表';
|
||||||
|
""")
|
||||||
|
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
conn.execute(create_table_query)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
logger.info("融资融券数据表创建成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"确保数据表存在失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_to_database(self, data: pd.DataFrame) -> bool:
|
||||||
|
"""
|
||||||
|
将数据保存到数据库
|
||||||
|
|
||||||
|
首次存储时,存储所有数据;
|
||||||
|
后续更新时,如果数据已存在则更新,不存在则插入
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: 要保存的数据DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功保存数据
|
||||||
|
"""
|
||||||
|
if data.empty:
|
||||||
|
logger.warning("没有数据需要保存")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 确保数据表存在
|
||||||
|
if not self._ensure_table_exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 添加数据或更新已有数据
|
||||||
|
inserted_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
for _, row in data.iterrows():
|
||||||
|
# 检查该日期的数据是否已存在
|
||||||
|
check_query = text("""
|
||||||
|
SELECT COUNT(*) FROM rzrq_data WHERE trade_date = :trade_date
|
||||||
|
""")
|
||||||
|
result = conn.execute(check_query, {"trade_date": row['trade_date']}).scalar()
|
||||||
|
|
||||||
|
if result > 0: # 数据已存在,执行更新
|
||||||
|
update_query = text("""
|
||||||
|
UPDATE rzrq_data SET
|
||||||
|
sh_financing_balance = :sh_financing_balance,
|
||||||
|
sz_financing_balance = :sz_financing_balance,
|
||||||
|
bj_financing_balance = :bj_financing_balance,
|
||||||
|
total_financing_balance = :total_financing_balance,
|
||||||
|
sh_financing_buy = :sh_financing_buy,
|
||||||
|
sz_financing_buy = :sz_financing_buy,
|
||||||
|
bj_financing_buy = :bj_financing_buy,
|
||||||
|
total_financing_buy = :total_financing_buy,
|
||||||
|
sh_securities_balance = :sh_securities_balance,
|
||||||
|
sz_securities_balance = :sz_securities_balance,
|
||||||
|
bj_securities_balance = :bj_securities_balance,
|
||||||
|
total_securities_balance = :total_securities_balance,
|
||||||
|
sh_rzrq_balance = :sh_rzrq_balance,
|
||||||
|
sz_rzrq_balance = :sz_rzrq_balance,
|
||||||
|
bj_rzrq_balance = :bj_rzrq_balance,
|
||||||
|
total_rzrq_balance = :total_rzrq_balance
|
||||||
|
WHERE trade_date = :trade_date
|
||||||
|
""")
|
||||||
|
conn.execute(update_query, row.to_dict())
|
||||||
|
updated_count += 1
|
||||||
|
else: # 数据不存在,执行插入
|
||||||
|
insert_query = text("""
|
||||||
|
INSERT INTO rzrq_data (
|
||||||
|
trade_date,
|
||||||
|
sh_financing_balance, sz_financing_balance, bj_financing_balance, total_financing_balance,
|
||||||
|
sh_financing_buy, sz_financing_buy, bj_financing_buy, total_financing_buy,
|
||||||
|
sh_securities_balance, sz_securities_balance, bj_securities_balance, total_securities_balance,
|
||||||
|
sh_rzrq_balance, sz_rzrq_balance, bj_rzrq_balance, total_rzrq_balance
|
||||||
|
) VALUES (
|
||||||
|
:trade_date,
|
||||||
|
:sh_financing_balance, :sz_financing_balance, :bj_financing_balance, :total_financing_balance,
|
||||||
|
:sh_financing_buy, :sz_financing_buy, :bj_financing_buy, :total_financing_buy,
|
||||||
|
:sh_securities_balance, :sz_securities_balance, :bj_securities_balance, :total_securities_balance,
|
||||||
|
:sh_rzrq_balance, :sz_rzrq_balance, :bj_rzrq_balance, :total_rzrq_balance
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute(insert_query, row.to_dict())
|
||||||
|
inserted_count += 1
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
logger.info(f"数据保存成功:新增 {inserted_count} 条记录,更新 {updated_count} 条记录")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存数据到数据库失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_latest_data(self) -> bool:
|
||||||
|
"""
|
||||||
|
更新最新一天的融资融券数据
|
||||||
|
|
||||||
|
仅获取第一页的第一行数据,如果数据库中已存在,则更新;否则插入
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功更新最新数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("开始更新最新一天的融资融券数据")
|
||||||
|
|
||||||
|
# 获取第一页数据
|
||||||
|
df = self.fetch_page_data(1)
|
||||||
|
if df.empty:
|
||||||
|
logger.warning("未获取到最新数据")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 只保留第一行(最新一天的数据)
|
||||||
|
latest_data = df.iloc[:1]
|
||||||
|
|
||||||
|
# 保存数据到数据库
|
||||||
|
result = self.save_to_database(latest_data)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info(f"最新数据({latest_data.iloc[0]['trade_date']})更新成功")
|
||||||
|
else:
|
||||||
|
logger.warning("最新数据更新失败")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新最新数据失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def initial_data_collection(self) -> bool:
|
||||||
|
"""
|
||||||
|
首次全量采集融资融券数据
|
||||||
|
|
||||||
|
采集所有页的数据并保存到数据库
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功采集所有数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("开始首次全量采集融资融券数据")
|
||||||
|
|
||||||
|
# 获取所有页的数据
|
||||||
|
df = self.fetch_all_data()
|
||||||
|
if df.empty:
|
||||||
|
logger.warning("未获取到任何数据")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 保存数据到数据库
|
||||||
|
result = self.save_to_database(df)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info(f"全量数据采集完成,共采集 {len(df)} 条记录")
|
||||||
|
else:
|
||||||
|
logger.warning("全量数据采集失败")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"首次全量采集失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_chart_data(self, limit_days: int = 30) -> dict:
|
||||||
|
"""
|
||||||
|
获取融资融券数据用于图表展示
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit_days: 获取最近多少天的数据,默认30天
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含图表所需数据的字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"获取最近 {limit_days} 天融资融券数据用于图表展示")
|
||||||
|
|
||||||
|
# 构建SQL查询
|
||||||
|
query = text("""
|
||||||
|
SELECT
|
||||||
|
trade_date,
|
||||||
|
total_financing_balance,
|
||||||
|
total_financing_buy,
|
||||||
|
total_securities_balance,
|
||||||
|
total_rzrq_balance
|
||||||
|
FROM rzrq_data
|
||||||
|
ORDER BY trade_date DESC
|
||||||
|
LIMIT :limit_days
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 执行查询
|
||||||
|
with self.engine.connect() as conn:
|
||||||
|
result = conn.execute(query, {"limit_days": limit_days}).fetchall()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
logger.warning("未找到融资融券数据")
|
||||||
|
return {"success": False, "message": "未找到融资融券数据"}
|
||||||
|
|
||||||
|
# 将结果转换为列表并倒序,使日期按升序排列
|
||||||
|
rows = []
|
||||||
|
for row in result:
|
||||||
|
# 将每行结果转换为字典
|
||||||
|
row_dict = {
|
||||||
|
'trade_date': row.trade_date,
|
||||||
|
'total_financing_balance': row.total_financing_balance,
|
||||||
|
'total_financing_buy': row.total_financing_buy,
|
||||||
|
'total_securities_balance': row.total_securities_balance,
|
||||||
|
'total_rzrq_balance': row.total_rzrq_balance
|
||||||
|
}
|
||||||
|
rows.append(row_dict)
|
||||||
|
|
||||||
|
# 反转列表使日期按升序排列
|
||||||
|
rows.reverse()
|
||||||
|
|
||||||
|
# 准备数据
|
||||||
|
dates = []
|
||||||
|
total_rzrq_balance = [] # 融资融券余额合计
|
||||||
|
total_financing_buy = [] # 融资买入额合计
|
||||||
|
total_financing_balance = [] # 融资余额合计
|
||||||
|
financing_repayment = [] # 融资偿还
|
||||||
|
securities_balance = [] # 融券余额
|
||||||
|
|
||||||
|
prev_financing_balance = None # 上一日融资余额
|
||||||
|
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
dates.append(row['trade_date'].strftime('%Y-%m-%d'))
|
||||||
|
total_rzrq_balance.append(float(row['total_rzrq_balance']))
|
||||||
|
total_financing_buy.append(float(row['total_financing_buy']))
|
||||||
|
total_financing_balance.append(float(row['total_financing_balance']))
|
||||||
|
|
||||||
|
# 计算融券余额 = 融资融券余额合计 - 融资余额合计
|
||||||
|
securities_bal = float(row['total_rzrq_balance']) - float(row['total_financing_balance'])
|
||||||
|
securities_balance.append(securities_bal)
|
||||||
|
|
||||||
|
# 计算融资偿还 = 融资买入额 + 前一日融资余额 - 当日融资余额
|
||||||
|
if i > 0 and prev_financing_balance is not None:
|
||||||
|
repayment = float(row['total_financing_buy']) + prev_financing_balance - float(row['total_financing_balance'])
|
||||||
|
financing_repayment.append(repayment)
|
||||||
|
else:
|
||||||
|
financing_repayment.append(None) # 第一天无法计算
|
||||||
|
|
||||||
|
prev_financing_balance = float(row['total_financing_balance'])
|
||||||
|
|
||||||
|
# 计算市场风险分析指标
|
||||||
|
risk_indicators = self.analyze_market_risk(
|
||||||
|
dates=dates,
|
||||||
|
total_rzrq_balance=total_rzrq_balance,
|
||||||
|
total_financing_balance=total_financing_balance,
|
||||||
|
total_financing_buy=total_financing_buy,
|
||||||
|
securities_balance=securities_balance
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
chart_data = {
|
||||||
|
"success": True,
|
||||||
|
"dates": dates,
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "融资融券余额合计",
|
||||||
|
"data": total_rzrq_balance,
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "融资买入额合计",
|
||||||
|
"data": total_financing_buy,
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "融资余额合计",
|
||||||
|
"data": total_financing_balance,
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "融资偿还",
|
||||||
|
"data": financing_repayment,
|
||||||
|
"unit": "亿元"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "融券余额",
|
||||||
|
"data": securities_balance,
|
||||||
|
"unit": "亿元"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"risk_indicators": risk_indicators
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加更新时间
|
||||||
|
chart_data["last_update"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
logger.info(f"成功获取 {len(dates)} 天的融资融券图表数据")
|
||||||
|
return chart_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取融资融券图表数据失败: {e}")
|
||||||
|
return {"success": False, "message": f"获取数据失败: {str(e)}"}
|
||||||
|
|
||||||
|
def analyze_market_risk(self, dates, total_rzrq_balance, total_financing_balance, total_financing_buy, securities_balance):
|
||||||
|
"""
|
||||||
|
分析融资融券数据,计算市场风险指标
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dates: 日期列表
|
||||||
|
total_rzrq_balance: 融资融券余额合计列表
|
||||||
|
total_financing_balance: 融资余额合计列表
|
||||||
|
total_financing_buy: 融资买入额合计列表
|
||||||
|
securities_balance: 融券余额列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含风险指标的字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 确保数据长度足够
|
||||||
|
if len(dates) < 5:
|
||||||
|
return {"warning": "数据量不足,无法进行完整分析"}
|
||||||
|
|
||||||
|
risk_indicators = {}
|
||||||
|
|
||||||
|
# 1. 计算融资融券余额趋势 - 近期变化率
|
||||||
|
recent_days = 5 # 分析最近5天的变化
|
||||||
|
if len(total_rzrq_balance) >= recent_days:
|
||||||
|
latest_balance = total_rzrq_balance[-1]
|
||||||
|
prev_balance = total_rzrq_balance[-recent_days]
|
||||||
|
|
||||||
|
if prev_balance > 0:
|
||||||
|
balance_change_rate = (latest_balance - prev_balance) / prev_balance * 100
|
||||||
|
balance_change_direction = "上升" if balance_change_rate > 0 else "下降"
|
||||||
|
|
||||||
|
risk_indicators["recent_balance_change"] = {
|
||||||
|
"rate": round(balance_change_rate, 2),
|
||||||
|
"direction": balance_change_direction,
|
||||||
|
"days": recent_days,
|
||||||
|
"start_date": dates[-recent_days],
|
||||||
|
"end_date": dates[-1],
|
||||||
|
"start_value": round(prev_balance, 2),
|
||||||
|
"end_value": round(latest_balance, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 风险评级 - 融资融券余额变化
|
||||||
|
if balance_change_rate > 10:
|
||||||
|
balance_risk_level = "高"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额快速上升{abs(round(balance_change_rate, 2))}%,市场杠杆水平快速提升,风险较高"
|
||||||
|
elif balance_change_rate > 5:
|
||||||
|
balance_risk_level = "中"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额上升{abs(round(balance_change_rate, 2))}%,市场杠杆水平有所提升"
|
||||||
|
elif balance_change_rate < -10:
|
||||||
|
balance_risk_level = "高"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额快速下降{abs(round(balance_change_rate, 2))}%,市场杠杆水平快速下降,可能伴随抛压"
|
||||||
|
elif balance_change_rate < -5:
|
||||||
|
balance_risk_level = "中"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额下降{abs(round(balance_change_rate, 2))}%,市场杠杆水平有所下降"
|
||||||
|
else:
|
||||||
|
balance_risk_level = "低"
|
||||||
|
balance_risk_desc = f"近{recent_days}天融资融券余额变化{round(balance_change_rate, 2)}%,市场杠杆水平相对稳定"
|
||||||
|
|
||||||
|
risk_indicators["balance_risk"] = {
|
||||||
|
"level": balance_risk_level,
|
||||||
|
"description": balance_risk_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. 计算融资偿还比例 - 衡量资金偿还压力
|
||||||
|
# 融资偿还比例 = 当日融资偿还 / 当日融资买入
|
||||||
|
recent_days_repay = min(5, len(total_financing_buy) - 1) # 考虑数据长度
|
||||||
|
if recent_days_repay > 0:
|
||||||
|
# 计算近几天的平均偿还买入比
|
||||||
|
repay_buy_ratios = []
|
||||||
|
for i in range(1, recent_days_repay + 1):
|
||||||
|
if total_financing_buy[-i] > 0:
|
||||||
|
# 融资偿还 = 当日融资买入额 + 前一日融资余额 - 当日融资余额
|
||||||
|
if i < len(total_financing_balance) and i+1 < len(total_financing_balance):
|
||||||
|
repayment = total_financing_buy[-i] + total_financing_balance[-(i+1)] - total_financing_balance[-i]
|
||||||
|
ratio = repayment / total_financing_buy[-i]
|
||||||
|
repay_buy_ratios.append(ratio)
|
||||||
|
|
||||||
|
if repay_buy_ratios:
|
||||||
|
avg_repay_buy_ratio = sum(repay_buy_ratios) / len(repay_buy_ratios)
|
||||||
|
|
||||||
|
risk_indicators["repay_buy_ratio"] = {
|
||||||
|
"value": round(avg_repay_buy_ratio, 2),
|
||||||
|
"days": recent_days_repay
|
||||||
|
}
|
||||||
|
|
||||||
|
# 风险评级 - 偿还率
|
||||||
|
if avg_repay_buy_ratio > 1.2:
|
||||||
|
repay_risk_level = "高"
|
||||||
|
repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},偿还明显大于买入,市场存在较强的抛压"
|
||||||
|
elif avg_repay_buy_ratio > 1.05:
|
||||||
|
repay_risk_level = "中"
|
||||||
|
repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},偿还略大于买入,市场抛压增加"
|
||||||
|
elif avg_repay_buy_ratio < 0.8:
|
||||||
|
repay_risk_level = "中"
|
||||||
|
repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},买入明显大于偿还,杠杆快速提升"
|
||||||
|
else:
|
||||||
|
repay_risk_level = "低"
|
||||||
|
repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},买入与偿还较为平衡"
|
||||||
|
|
||||||
|
risk_indicators["repay_risk"] = {
|
||||||
|
"level": repay_risk_level,
|
||||||
|
"description": repay_risk_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. 融券余额变化 - 分析空头力量
|
||||||
|
if len(securities_balance) >= recent_days:
|
||||||
|
latest_securities = securities_balance[-1]
|
||||||
|
prev_securities = securities_balance[-recent_days]
|
||||||
|
|
||||||
|
if prev_securities > 0:
|
||||||
|
securities_change_rate = (latest_securities - prev_securities) / prev_securities * 100
|
||||||
|
securities_change_direction = "上升" if securities_change_rate > 0 else "下降"
|
||||||
|
|
||||||
|
risk_indicators["securities_balance_change"] = {
|
||||||
|
"rate": round(securities_change_rate, 2),
|
||||||
|
"direction": securities_change_direction,
|
||||||
|
"days": recent_days,
|
||||||
|
"start_value": round(prev_securities, 2),
|
||||||
|
"end_value": round(latest_securities, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 风险评级 - 融券余额变化
|
||||||
|
if securities_change_rate > 15:
|
||||||
|
securities_risk_level = "高"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额快速上升{abs(round(securities_change_rate, 2))}%,空头力量显著增强"
|
||||||
|
elif securities_change_rate > 8:
|
||||||
|
securities_risk_level = "中"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额上升{abs(round(securities_change_rate, 2))}%,空头力量有所增强"
|
||||||
|
elif securities_change_rate < -15:
|
||||||
|
securities_risk_level = "低"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额快速下降{abs(round(securities_change_rate, 2))}%,空头力量显著减弱"
|
||||||
|
elif securities_change_rate < -8:
|
||||||
|
securities_risk_level = "低"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额下降{abs(round(securities_change_rate, 2))}%,空头力量有所减弱"
|
||||||
|
else:
|
||||||
|
securities_risk_level = "正常"
|
||||||
|
securities_risk_desc = f"近{recent_days}天融券余额变化{round(securities_change_rate, 2)}%,空头力量相对稳定"
|
||||||
|
|
||||||
|
risk_indicators["securities_risk"] = {
|
||||||
|
"level": securities_risk_level,
|
||||||
|
"description": securities_risk_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. 融资占比 - 融资余额占融资融券余额的比例
|
||||||
|
if total_rzrq_balance[-1] > 0:
|
||||||
|
financing_ratio = total_financing_balance[-1] / total_rzrq_balance[-1]
|
||||||
|
risk_indicators["financing_ratio"] = round(financing_ratio * 100, 2)
|
||||||
|
|
||||||
|
# 融资占比的历史百分位数计算
|
||||||
|
if len(total_rzrq_balance) > 20: # 至少需要20天数据计算有意义的百分位数
|
||||||
|
historical_ratios = []
|
||||||
|
for i in range(len(total_rzrq_balance)):
|
||||||
|
if total_rzrq_balance[i] > 0:
|
||||||
|
ratio = total_financing_balance[i] / total_rzrq_balance[i]
|
||||||
|
historical_ratios.append(ratio)
|
||||||
|
|
||||||
|
if historical_ratios:
|
||||||
|
# 计算融资占比的百分位数
|
||||||
|
sorted_ratios = sorted(historical_ratios)
|
||||||
|
percentile_rank = sorted_ratios.index(financing_ratio) / len(sorted_ratios) * 100 if financing_ratio in sorted_ratios else 0
|
||||||
|
|
||||||
|
# 如果不在列表中,找到最接近的位置
|
||||||
|
if percentile_rank == 0:
|
||||||
|
for i, ratio in enumerate(sorted_ratios):
|
||||||
|
if ratio > financing_ratio:
|
||||||
|
percentile_rank = i / len(sorted_ratios) * 100
|
||||||
|
break
|
||||||
|
if percentile_rank == 0 and financing_ratio > sorted_ratios[-1]:
|
||||||
|
percentile_rank = 100
|
||||||
|
|
||||||
|
risk_indicators["financing_ratio_percentile"] = round(percentile_rank, 2)
|
||||||
|
|
||||||
|
# 5. 综合风险评估
|
||||||
|
risk_levels = []
|
||||||
|
if "balance_risk" in risk_indicators:
|
||||||
|
risk_levels.append(risk_indicators["balance_risk"]["level"])
|
||||||
|
if "repay_risk" in risk_indicators:
|
||||||
|
risk_levels.append(risk_indicators["repay_risk"]["level"])
|
||||||
|
if "securities_risk" in risk_indicators:
|
||||||
|
risk_levels.append(risk_indicators["securities_risk"]["level"])
|
||||||
|
|
||||||
|
# 计算综合风险等级
|
||||||
|
high_count = risk_levels.count("高")
|
||||||
|
medium_count = risk_levels.count("中")
|
||||||
|
|
||||||
|
if high_count >= 2:
|
||||||
|
overall_risk = "高"
|
||||||
|
overall_desc = "多个指标显示市场风险较高,融资融券数据反映市场杠杆或波动风险增加"
|
||||||
|
elif high_count == 1 or medium_count >= 2:
|
||||||
|
overall_risk = "中"
|
||||||
|
overall_desc = "部分指标显示市场风险增加,建议关注融资融券数据变化"
|
||||||
|
else:
|
||||||
|
overall_risk = "低"
|
||||||
|
overall_desc = "融资融券数据显示市场风险较低,杠杆水平相对稳定"
|
||||||
|
|
||||||
|
risk_indicators["overall_risk"] = {
|
||||||
|
"level": overall_risk,
|
||||||
|
"description": overall_desc
|
||||||
|
}
|
||||||
|
|
||||||
|
return risk_indicators
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"分析市场风险指标失败: {e}")
|
||||||
|
return {"error": f"分析失败: {str(e)}"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_create_table_sql() -> str:
|
||||||
|
"""
|
||||||
|
获取创建融资融券数据表的SQL语句
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
创建表的SQL语句
|
||||||
|
"""
|
||||||
|
return """
|
||||||
|
CREATE TABLE IF NOT EXISTS rzrq_data (
|
||||||
|
trade_date DATE PRIMARY KEY,
|
||||||
|
sh_financing_balance DECIMAL(12,2) COMMENT '上海融资余额(亿元)',
|
||||||
|
sz_financing_balance DECIMAL(12,2) COMMENT '深圳融资余额(亿元)',
|
||||||
|
bj_financing_balance DECIMAL(12,2) COMMENT '北京融资余额(亿元)',
|
||||||
|
total_financing_balance DECIMAL(12,2) COMMENT '融资余额合计(亿元)',
|
||||||
|
sh_financing_buy DECIMAL(12,2) COMMENT '上海融资买入额(亿元)',
|
||||||
|
sz_financing_buy DECIMAL(12,2) COMMENT '深圳融资买入额(亿元)',
|
||||||
|
bj_financing_buy DECIMAL(12,2) COMMENT '北京融资买入额(亿元)',
|
||||||
|
total_financing_buy DECIMAL(12,2) COMMENT '融资买入额合计(亿元)',
|
||||||
|
sh_securities_balance DECIMAL(12,2) COMMENT '上海融券余量余额(亿元)',
|
||||||
|
sz_securities_balance DECIMAL(12,2) COMMENT '深圳融券余量余额(亿元)',
|
||||||
|
bj_securities_balance DECIMAL(12,2) COMMENT '北京融券余量余额(亿元)',
|
||||||
|
total_securities_balance DECIMAL(12,2) COMMENT '融券余量余额合计(亿元)',
|
||||||
|
sh_rzrq_balance DECIMAL(12,2) COMMENT '上海融资融券余额(亿元)',
|
||||||
|
sz_rzrq_balance DECIMAL(12,2) COMMENT '深圳融资融券余额(亿元)',
|
||||||
|
bj_rzrq_balance DECIMAL(12,2) COMMENT '北京融资融券余额(亿元)',
|
||||||
|
total_rzrq_balance DECIMAL(12,2) COMMENT '融资融券余额合计(亿元)',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='融资融券数据表';
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# 示例使用方式
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 创建融资融券数据采集器
|
||||||
|
collector = RzrqCollector()
|
||||||
|
|
||||||
|
# 输出创建表的SQL语句
|
||||||
|
print("创建表的SQL语句:")
|
||||||
|
print(get_create_table_sql())
|
||||||
|
|
||||||
|
# 首次全量采集数据
|
||||||
|
print("\n开始首次全量采集数据...")
|
||||||
|
collector.initial_data_collection()
|
|
@ -0,0 +1,91 @@
|
||||||
|
阶段一:数据准备与基础指标计算
|
||||||
|
|
||||||
|
数据加载:使用Pandas加载您提供的rzrq_data.csv融资融券数据。未来若引入市场指数数据(如上证综指、深证成指的收盘价、成交量等),也一并加载,并按日期对齐。
|
||||||
|
基础指标计算:根据上一轮讨论的8个指标,计算出每日的指标值。例如:
|
||||||
|
融资余额净变动 = total_financing_balance.diff()
|
||||||
|
融资买入活跃度 = total_financing_buy / total_financing_balance.shift(1)
|
||||||
|
融券余量净变动 = total_securities_balance.diff()
|
||||||
|
融资融券比 = total_financing_balance / total_securities_balance (注意处理分母为0的情况)
|
||||||
|
融资买入额趋势:可通过计算total_financing_buy的短期均线与长期均线的关系(如金叉/死叉,或差值)。
|
||||||
|
市场总杠杆水平趋势:可通过观察total_rzrq_balance的变动或其均线趋势。
|
||||||
|
融资偿还额 = total_financing_balance.shift(1) + total_financing_buy - total_financing_balance
|
||||||
|
阶段二:指标标准化与信号转换
|
||||||
|
|
||||||
|
对每个基础指标,我们需要将其转化为一个“信号分”,这个分数应能反映该指标当前状态所指示的“机会”或“风险”的强度。
|
||||||
|
|
||||||
|
标准化方法(选其一或组合):
|
||||||
|
|
||||||
|
Z-Score 标准化: (value - mean) / std_dev。计算指标在一定历史回溯期(如过去1年,约252个交易日)的Z-Score。Z-Score能表示当前值偏离历史均值的程度。
|
||||||
|
Python实现: 可以用scipy.stats.zscore或手动计算滚动Z-score (series - series.rolling(window=N).mean()) / series.rolling(window=N).std()。
|
||||||
|
百分位排名 (Percentile Ranking): 计算当前值在历史回溯期数据中的百分位。例如,0%表示历史最低,100%表示历史最高。
|
||||||
|
Python实现: scipy.stats.percentileofscore 或 series.rolling(window=N).apply(lambda x: pd.Series(x).rank(pct=True).iloc[-1])。
|
||||||
|
Min-Max 标准化: 将数值缩放到特定区间,如 [-1, 1] 或 [0, 1]。 scaled = (value - min_val) / (max_val - min_val)。
|
||||||
|
Python实现: sklearn.preprocessing.MinMaxScaler 或手动计算。
|
||||||
|
信号分转换逻辑(示例):
|
||||||
|
假设我们将每个指标的信号分努力映射到 [-2, +2] 的范围,其中正数代表机会,负数代表风险,绝对值大小代表强度。
|
||||||
|
|
||||||
|
融资余额净变动 (fin_balance_change_z):
|
||||||
|
Z-Score本身可直接作为信号的初步强度。例如,直接使用Z-Score,然后用np.clip(fin_balance_change_z, -2, 2)将其限制在[-2, 2]内。
|
||||||
|
融资买入活跃度 (fin_buy_activity_z):
|
||||||
|
通常活跃度高是正面信号,但极高可能过热。可以对其Z-Score进行评估。
|
||||||
|
信号分 score_fin_buy_activity = np.clip(fin_buy_activity_z, -2, 2)。
|
||||||
|
融券余量净变动 (sec_balance_change_z):
|
||||||
|
融券增加是负面信号。
|
||||||
|
信号分 score_sec_balance_change = np.clip(-sec_balance_change_z, -2, 2) (注意取负号)。
|
||||||
|
融资融券比 (fin_to_sec_ratio_z):
|
||||||
|
通常比率高且持续上升是正面信号。可以对其Z-Score或其变动的Z-Score进行评估。
|
||||||
|
信号分 score_fin_to_sec_ratio = np.clip(fin_to_sec_ratio_z, -2, 2)。
|
||||||
|
融资买入额趋势:
|
||||||
|
例如,短期均线上穿长期均线视为+1,死叉为-1。或者计算(短期MA / 长期MA - 1)的Z-Score。
|
||||||
|
信号分 score_fin_buy_trend = np.clip(trend_z_score, -2, 2)。
|
||||||
|
市场总杠杆水平趋势 (total_rzrq_balance_change_z or total_rzrq_balance_level_z):
|
||||||
|
杠杆水平过高可能是风险,但温和上涨可能代表市场信心。可以分析其变动率的Z-Score。如果绝对水平过高(如处于历史90%分位以上),可以额外增加风险权重。
|
||||||
|
信号分 score_total_leverage_trend = np.clip(total_rzrq_balance_change_z, -2, 2)。同时,如果total_rzrq_balance_level_z > 1.5 (例如),则可以额外扣分。
|
||||||
|
融资偿还额 (fin_repayment_z):
|
||||||
|
偿还额大幅增加通常是负面信号。
|
||||||
|
信号分 score_fin_repayment = np.clip(-fin_repayment_z, -2, 2) (注意取负号)。
|
||||||
|
(注意:以上转换逻辑和阈值是示例,需要根据实际数据分布和市场理解进行细化和调整。)
|
||||||
|
|
||||||
|
阶段三:(可选) 引入市场数据指标
|
||||||
|
|
||||||
|
如果引入了市场指数数据(如上证综指),可以计算额外的市场指标:
|
||||||
|
指数趋势: 如指数收盘价与其移动平均线(MA20, MA60)的关系,MACD指标等。
|
||||||
|
市场波动率: 如指数的实际波动率。
|
||||||
|
成交量趋势: 成交量与其移动平均线的关系。
|
||||||
|
这些市场指标也需要进行标准化和信号转换,赋予其相应的机会/风险评分。
|
||||||
|
阶段四:综合得分计算
|
||||||
|
|
||||||
|
权重设定 (weights): 为每个指标的信号分(score_indicator_X)分配权重。
|
||||||
|
Python
|
||||||
|
|
||||||
|
weights = {
|
||||||
|
'score_fin_balance_change': 1.0,
|
||||||
|
'score_fin_buy_activity': 0.8,
|
||||||
|
'score_sec_balance_change': 1.2,
|
||||||
|
'score_fin_to_sec_ratio': 1.0,
|
||||||
|
'score_fin_buy_trend': 1.0,
|
||||||
|
'score_total_leverage_trend': 0.8,
|
||||||
|
'score_fin_repayment': 1.2,
|
||||||
|
# 'score_market_index_trend': 1.5, # 如果引入市场指标
|
||||||
|
}
|
||||||
|
加权求和: composite_score = Σ (score_indicator_X * weight_X) Python实现:
|
||||||
|
Python
|
||||||
|
|
||||||
|
df['composite_score'] = 0
|
||||||
|
total_weight = 0
|
||||||
|
for indicator_name, weight_value in weights.items():
|
||||||
|
df['composite_score'] += df[indicator_name] * weight_value
|
||||||
|
total_weight += weight_value
|
||||||
|
# (可选) 如果希望将composite_score也标准化到一个固定范围,可以除以total_weight或者再次进行Z-Score或Min-Max
|
||||||
|
# df['composite_score_normalized'] = df['composite_score'] / total_weight
|
||||||
|
阶段五:结果解读与分级
|
||||||
|
|
||||||
|
定义阈值: 根据历史composite_score的分布,定义不同市场状态的阈值。 例如,假设composite_score经过处理后大致分布在 -10 到 +10 之间:
|
||||||
|
> 7: 强机会 (Strong Opportunity)
|
||||||
|
3 to 7: 中等机会 (Moderate Opportunity)
|
||||||
|
0.5 to 3: 轻微机会 (Slight Opportunity)
|
||||||
|
-0.5 to 0.5: 中性 (Neutral)
|
||||||
|
-3 to -0.5: 轻微风险 (Slight Risk)
|
||||||
|
-7 to -3: 中等风险 (Moderate Risk)
|
||||||
|
< -7: 强风险 (Strong Risk)
|
||||||
|
数值化程度: composite_score的绝对值可以作为“危险”或“机会”的数值化程度。或者,将其映射到一个百分比,例如,如果最大正向得分为10,当前得分7,则机会程度为70%。
|
Loading…
Reference in New Issue