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
|
||||
redis==5.2.1
|
||||
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.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(
|
||||
level=logging.INFO,
|
||||
|
@ -65,6 +80,15 @@ industry_analyzer = IndustryAnalyzer()
|
|||
# 创建监控器实例
|
||||
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__)))
|
||||
REPORTS_DIR = os.path.join(ROOT_DIR, 'src', 'reports')
|
||||
|
@ -76,6 +100,9 @@ logger.info(f"报告目录路径: {REPORTS_DIR}")
|
|||
# 存储回测任务状态的字典
|
||||
backtest_tasks = {}
|
||||
|
||||
# 融资融券数据采集任务列表
|
||||
rzrq_tasks = {}
|
||||
|
||||
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)
|
||||
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('/')
|
||||
def index():
|
||||
"""渲染主页"""
|
||||
|
@ -1558,6 +1733,45 @@ def get_industry_list():
|
|||
"message": f"获取行业列表失败: {str(e)}"
|
||||
}), 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'])
|
||||
def industry_analysis():
|
||||
"""
|
||||
|
@ -1875,6 +2089,67 @@ def get_southbound_data():
|
|||
"message": f"服务器错误: {str(e)}"
|
||||
}), 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'])
|
||||
def get_stock_tracks():
|
||||
"""根据股票代码获取相关赛道信息
|
||||
|
@ -1939,5 +2214,451 @@ def get_stock_tracks():
|
|||
"message": f"服务器错误: {str(e)}"
|
||||
}), 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__':
|
||||
"""
|
||||
# 手动释放锁的方法(需要时取消注释)
|
||||
# 创建锁实例
|
||||
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)
|
|
@ -11,7 +11,7 @@ XUEQIU_HEADERS = {
|
|||
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'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',
|
||||
'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
# coding:utf-8
|
||||
|
||||
import requests
|
||||
|
@ -6,9 +5,8 @@ import pandas as pd
|
|||
from sqlalchemy import create_engine, text
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
from config import XUEQIU_HEADERS
|
||||
from src.scripts.config import XUEQIU_HEADERS
|
||||
import gc
|
||||
import time
|
||||
|
||||
class StockDailyDataCollector:
|
||||
"""股票日线数据采集器类"""
|
||||
|
@ -23,9 +21,20 @@ class StockDailyDataCollector:
|
|||
self.headers = XUEQIU_HEADERS
|
||||
|
||||
def fetch_all_stock_codes(self):
|
||||
query = "SELECT gp_code FROM gp_code_all"
|
||||
df = pd.read_sql(query, self.engine)
|
||||
return df['gp_code'].tolist()
|
||||
# 从gp_code_all获取股票代码
|
||||
query_all = "SELECT gp_code FROM gp_code_all"
|
||||
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):
|
||||
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 southChart = null;
|
||||
let rzrqChart = null; // 融资融券图表实例
|
||||
|
||||
// 当前显示的融资融券数据系列
|
||||
let currentMetric = 'total_rzrq_balance';
|
||||
// 融资融券数据
|
||||
let rzrqData = null;
|
||||
|
||||
// 融资融券图表相关功能
|
||||
let rzrqIndexSelector = null;
|
||||
let rzrqChartData = null; // 用于存储融资融券图表的原始数据
|
||||
|
||||
// 初始化图表函数,确保DOM元素存在
|
||||
function initCharts() {
|
||||
try {
|
||||
const northChartDom = document.getElementById('northChart');
|
||||
const southChartDom = document.getElementById('southChart');
|
||||
const rzrqChartDom = document.getElementById('rzrqChart');
|
||||
|
||||
if (northChartDom && !northChart) {
|
||||
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) {
|
||||
console.error('图表初始化过程中发生错误:', e);
|
||||
return false;
|
||||
|
@ -48,10 +68,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// 开始加载数据
|
||||
loadData();
|
||||
|
||||
// 加载融资融券数据
|
||||
initRzrqChart();
|
||||
|
||||
// 检查是否在交易时段,只有在交易时段才设置自动刷新
|
||||
if (isWithinTradingHours()) {
|
||||
console.log('当前处于交易时段,启用自动刷新');
|
||||
// 设置自动刷新 (每分钟刷新一次)
|
||||
setInterval(loadData, 60000);
|
||||
window.refreshInterval = setInterval(loadData, 60000);
|
||||
} else {
|
||||
console.log('当前不在交易时段,不启用自动刷新');
|
||||
}
|
||||
}, 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() {
|
||||
if (northChart) {
|
||||
|
@ -68,6 +124,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
console.error('南向资金图表调整大小失败:', e);
|
||||
}
|
||||
}
|
||||
if (rzrqChart) {
|
||||
try {
|
||||
rzrqChart.resize();
|
||||
} catch (e) {
|
||||
console.error('融资融券图表调整大小失败:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 刷新按钮事件
|
||||
|
@ -75,9 +138,56 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
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');
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 融资融券图表相关功能 ============
|
||||
|
||||
/**
|
||||
* 初始化融资融券图表
|
||||
*/
|
||||
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;
|
||||
industryNameSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// 如果存在Select2,刷新它
|
||||
if ($.fn.select2 && $(industryNameSelect).data('select2')) {
|
||||
$(industryNameSelect).trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -734,6 +739,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
function resetForm() {
|
||||
industryForm.reset();
|
||||
|
||||
// 重置Select2
|
||||
if ($.fn.select2) {
|
||||
$(industryNameSelect).val('').trigger('change');
|
||||
}
|
||||
|
||||
// 隐藏结果和错误信息
|
||||
resultCard.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 startDateInput = document.getElementById('startDate');
|
||||
const metricSelect = document.getElementById('metric');
|
||||
const industryNameInput = document.getElementById('industryName');
|
||||
const conceptNameInput = document.getElementById('conceptName');
|
||||
const industryNameSelect = document.getElementById('industryName');
|
||||
const conceptNameSelect = document.getElementById('conceptName');
|
||||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const loadingSpinner = document.getElementById('loadingSpinner');
|
||||
|
@ -26,6 +26,128 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// 定义图表实例
|
||||
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) {
|
||||
event.preventDefault();
|
||||
|
@ -52,8 +174,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const stockCode = stockCodeInput.value.trim();
|
||||
const startDate = startDateInput.value;
|
||||
const metric = metricSelect.value;
|
||||
const industryName = industryNameInput.value.trim();
|
||||
const conceptName = conceptNameInput.value.trim();
|
||||
const industryName = industryNameSelect.value.trim();
|
||||
const conceptName = conceptNameSelect.value.trim();
|
||||
|
||||
// 构建请求URL
|
||||
let url = `/api/valuation_analysis?stock_code=${stockCode}&start_date=${startDate}&metric=${metric}`;
|
||||
|
@ -452,6 +574,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
function resetForm() {
|
||||
valuationForm.reset();
|
||||
|
||||
// 重置Select2下拉框
|
||||
$(industryNameSelect).val('').trigger('change');
|
||||
$(conceptNameSelect).val('').trigger('change');
|
||||
|
||||
// 隐藏结果和错误信息
|
||||
resultCard.classList.add('d-none');
|
||||
errorAlert.classList.add('d-none');
|
||||
|
|
|
@ -6,60 +6,33 @@
|
|||
<title>沪深港通资金流向监控</title>
|
||||
<!-- Bootstrap 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>
|
||||
<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="row mb-4">
|
||||
<div class="col-12">
|
||||
|
@ -140,21 +113,202 @@
|
|||
</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="col-12">
|
||||
<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 class="card-body">
|
||||
<ul>
|
||||
<li>数据来源:同花顺数据,每分钟更新</li>
|
||||
<li><strong>北向资金</strong>:是指从<strong>香港</strong>流入<strong>A股</strong>的资金,通过沪股通和深股通进入</li>
|
||||
<li><strong>南向资金</strong>:是指从<strong>内地</strong>流入<strong>港股</strong>的资金,通过沪市港股通和深市港股通进入</li>
|
||||
<li>净流入为正表示买入大于卖出,资金流入(<span class="money-inflow">红色</span>);净流入为负表示卖出大于买入,资金流出(<span class="money-outflow">绿色</span>)</li>
|
||||
<li>交易时间:北向9:30-11:30, 13:00-15:00;南向9:30-12:00, 13:00-16:00</li>
|
||||
</ul>
|
||||
<div id="rzrqChart" class="chart-container"></div>
|
||||
<p class="update-time text-center" id="rzrqUpdateTime"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 融资融券风险分析 -->
|
||||
<!-- <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>
|
||||
|
@ -166,5 +320,544 @@
|
|||
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="../static/js/echarts.min.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>
|
||||
</html>
|
|
@ -10,8 +10,25 @@
|
|||
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- 引入ECharts -->
|
||||
<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 -->
|
||||
<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>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
|
@ -29,6 +46,9 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link" href="/industry">行业估值分析</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/hsgt">资金情况</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -64,12 +84,18 @@
|
|||
|
||||
<div class="col-md-4">
|
||||
<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 class="col-md-4">
|
||||
<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 class="col-12 text-center mt-4">
|
||||
|
@ -148,5 +174,39 @@
|
|||
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- 引入自定义JS -->
|
||||
<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>
|
||||
</html>
|
|
@ -10,8 +10,25 @@
|
|||
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- 引入ECharts -->
|
||||
<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 -->
|
||||
<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>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
|
@ -29,6 +46,9 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/industry">行业估值分析</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/hsgt">资金情况</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -45,7 +65,7 @@
|
|||
<form id="industryForm" class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<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>
|
||||
<!-- 将通过API动态填充 -->
|
||||
</select>
|
||||
|
@ -165,5 +185,21 @@
|
|||
<script src="../static/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- 引入行业分析JS -->
|
||||
<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>
|
||||
</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 sys
|
||||
import logging
|
||||
import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
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()
|
||||
import sys
|
||||
import os
|
||||
|
||||
from . import pe_pb_analysis
|
||||
from . import industry_analysis
|
||||
from . import rzrq_collector
|
||||
from .config import OUTPUT_DIR
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
args = parse_args()
|
||||
"""命令行工具主函数"""
|
||||
|
||||
# 解析参数
|
||||
stock_code = args.stock
|
||||
start_date = args.start_date
|
||||
metrics = args.metrics.split(',')
|
||||
output_format = args.format
|
||||
parser = argparse.ArgumentParser(description='股票估值分析工具', formatter_class=argparse.RawTextHelpFormatter)
|
||||
subparsers = parser.add_subparsers(dest='command', help='子命令')
|
||||
|
||||
# 设置输出路径
|
||||
output_path = args.output
|
||||
if output_path is None:
|
||||
output_path = OUTPUT_DIR / f"{stock_code}_valuation_analysis.{output_format}"
|
||||
else:
|
||||
output_path = Path(output_path)
|
||||
# 设置PE/PB分析子命令
|
||||
pepb_parser = subparsers.add_parser('pepb', help='PE/PB分析')
|
||||
pepb_parser.add_argument('--stock', '-s', required=True, help='股票代码')
|
||||
pepb_parser.add_argument('--days', '-d', type=int, default=1000, help='分析天数 (默认: 1000)')
|
||||
pepb_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json',
|
||||
help='输出格式 (默认: json)')
|
||||
|
||||
# 运行分析
|
||||
analyzer = ValuationAnalyzer()
|
||||
result = analyzer.analyze_stock_valuation(stock_code, start_date, metrics)
|
||||
# 设置ROE分析子命令
|
||||
roe_parser = subparsers.add_parser('roe', help='ROE分析')
|
||||
roe_parser.add_argument('--stock', '-s', required=True, help='股票代码')
|
||||
roe_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json',
|
||||
help='输出格式 (默认: json)')
|
||||
|
||||
# 设置行业分析子命令
|
||||
industry_parser = subparsers.add_parser('industry', help='行业估值分析')
|
||||
industry_parser.add_argument('--name', '-n', required=True, help='行业名称')
|
||||
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)')
|
||||
|
||||
# 设置行业列表子命令
|
||||
industry_list_parser = subparsers.add_parser('industry-list', help='获取行业列表')
|
||||
|
||||
# 设置融资融券数据采集子命令
|
||||
rzrq_parser = subparsers.add_parser('rzrq', help='融资融券数据采集')
|
||||
rzrq_parser.add_argument('--action', '-a', choices=['init', 'update', 'run-scheduler'],
|
||||
required=True, help='操作类型: init-首次全量采集,update-更新最新数据,run-scheduler-运行定时器')
|
||||
rzrq_parser.add_argument('--output-sql', '-s', action='store_true',
|
||||
help='输出创建表的SQL语句,仅与init配合使用')
|
||||
|
||||
# 解析命令行参数
|
||||
args = parser.parse_args()
|
||||
|
||||
# 如果没有提供子命令,显示帮助信息
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# 执行对应的子命令
|
||||
if args.command == 'pepb':
|
||||
# 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')
|
||||
|
||||
analyzer = pe_pb_analysis.StockValuationAnalyzer()
|
||||
result = analyzer.get_stock_pe_pb_analysis(args.stock, start_date, end_date)
|
||||
|
||||
if result["success"]:
|
||||
# 输出结果
|
||||
if not result['success']:
|
||||
print(f"分析失败: {result.get('message', '未知错误')}")
|
||||
return 1
|
||||
|
||||
# 打印分析结果
|
||||
stock_name = result['stock_name']
|
||||
analysis_date = result['analysis_date']
|
||||
|
||||
if output_format == 'json':
|
||||
# 将图表路径转换为相对路径字符串
|
||||
for metric in result['metrics']:
|
||||
if 'chart_path' in result['metrics'][metric]:
|
||||
result['metrics'][metric]['chart_path'] = str(result['metrics'][metric]['chart_path'])
|
||||
|
||||
# 写入JSON文件
|
||||
with open(output_path, 'w', encoding='utf-8') as 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}")
|
||||
|
||||
print(f"分析结果已保存至: {output_path}")
|
||||
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("\n" + "="*50)
|
||||
print(f"股票代码: {stock_code}")
|
||||
print(f"股票名称: {stock_name}")
|
||||
print(f"分析日期: {analysis_date}")
|
||||
print("="*50)
|
||||
print(f"分析失败: {result['message']}")
|
||||
|
||||
for metric in result['metrics']:
|
||||
metric_data = result['metrics'][metric]
|
||||
metric_name = "PE" if metric == "pe" else "PB"
|
||||
elif args.command == 'roe':
|
||||
# ROE分析
|
||||
analyzer = pe_pb_analysis.StockValuationAnalyzer()
|
||||
result = analyzer.get_stock_roe_analysis(args.stock)
|
||||
|
||||
print(f"\n{metric_name}分析结果:")
|
||||
print("-"*30)
|
||||
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']}")
|
||||
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("\n" + "="*50)
|
||||
print(f"分析完成,图表已保存")
|
||||
# 打印简要结果
|
||||
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']}")
|
||||
|
||||
return 0
|
||||
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__":
|
||||
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
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import pandas as pd
|
||||
import time
|
||||
|
||||
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:
|
||||
# 发送请求
|
||||
|
|
|
@ -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}")
|
||||
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]:
|
||||
"""
|
||||
获取指定行业的所有股票代码
|
||||
|
|
|
@ -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