This commit is contained in:
liao 2025-05-19 17:02:52 +08:00
parent 7f478d91f4
commit 5b9ae03000
26 changed files with 4899 additions and 219 deletions

View File

@ -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;

View File

@ -16,3 +16,4 @@ markdown2>=2.5.3
google-genai
redis==5.2.1
pandas==2.2.3
apscheduler==3.11.0

View File

@ -980,3 +980,5 @@ curl -X POST http://localhost:5000/api/comprehensive_analysis \
}'
```
PE--top bottom
PB--top bottom

View File

@ -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)

View File

@ -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',

View File

@ -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

View File

@ -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; /* 蓝色 (中性色) */
}

1
src/static/css/select2.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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;
}
});

View File

@ -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');

2
src/static/js/select2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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');

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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)}"
}

View File

@ -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:
# 发送请求

View File

@ -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]}")

View File

@ -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]:
"""
获取指定行业的所有股票代码

View File

@ -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()

View File

@ -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%。