commit;
This commit is contained in:
parent
0920615bbd
commit
7f478d91f4
169
src/app.py
169
src/app.py
|
@ -27,6 +27,9 @@ from src.valuation_analysis.pe_pb_analysis import ValuationAnalyzer
|
|||
# 导入行业估值分析器
|
||||
from src.valuation_analysis.industry_analysis import IndustryAnalyzer
|
||||
|
||||
# 导入沪深港通监控器
|
||||
from src.valuation_analysis.hsgt_monitor import HSGTMonitor
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
|
@ -59,6 +62,9 @@ valuation_analyzer = ValuationAnalyzer()
|
|||
# 创建行业分析器实例
|
||||
industry_analyzer = IndustryAnalyzer()
|
||||
|
||||
# 创建监控器实例
|
||||
hsgt_monitor = HSGTMonitor()
|
||||
|
||||
# 获取项目根目录
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
REPORTS_DIR = os.path.join(ROOT_DIR, 'src', 'reports')
|
||||
|
@ -1770,5 +1776,168 @@ def industry_page():
|
|||
"""渲染行业分析页面"""
|
||||
return render_template('industry.html')
|
||||
|
||||
@app.route('/hsgt')
|
||||
def hsgt_page():
|
||||
"""渲染沪深港通监控页面"""
|
||||
return render_template('hsgt_monitor.html')
|
||||
|
||||
@app.route('/api/hsgt/northbound', methods=['GET'])
|
||||
def get_northbound_data():
|
||||
"""获取北向资金流向数据接口
|
||||
|
||||
参数:
|
||||
- start_time: 可选,开始时间戳(秒)
|
||||
- end_time: 可选,结束时间戳(秒)
|
||||
|
||||
返回北向资金流向数据
|
||||
"""
|
||||
try:
|
||||
# 获取请求参数
|
||||
start_time = request.args.get('start_time')
|
||||
end_time = request.args.get('end_time')
|
||||
|
||||
# 转换为整数
|
||||
if start_time:
|
||||
start_time = int(start_time)
|
||||
if end_time:
|
||||
end_time = int(end_time)
|
||||
|
||||
# 调用数据获取方法
|
||||
result = hsgt_monitor.fetch_northbound_data(start_time, end_time)
|
||||
|
||||
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/hsgt/southbound', methods=['GET'])
|
||||
def get_southbound_data():
|
||||
"""获取南向资金流向数据接口
|
||||
|
||||
参数:
|
||||
- start_time: 可选,开始时间戳(秒)
|
||||
- end_time: 可选,结束时间戳(秒)
|
||||
|
||||
返回南向资金流向数据
|
||||
"""
|
||||
try:
|
||||
# 获取请求参数
|
||||
start_time = request.args.get('start_time')
|
||||
end_time = request.args.get('end_time')
|
||||
|
||||
# 转换为整数
|
||||
if start_time:
|
||||
start_time = int(start_time)
|
||||
if end_time:
|
||||
end_time = int(end_time)
|
||||
|
||||
# 调用数据获取方法
|
||||
result = hsgt_monitor.fetch_southbound_data(start_time, end_time)
|
||||
|
||||
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():
|
||||
"""根据股票代码获取相关赛道信息
|
||||
|
||||
参数:
|
||||
- stock_code: 必须,股票代码
|
||||
|
||||
返回赛道列表
|
||||
"""
|
||||
try:
|
||||
# 获取股票代码参数
|
||||
stock_code = request.args.get('stock_code')
|
||||
|
||||
# 验证参数
|
||||
if not stock_code:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "缺少必要参数: stock_code"
|
||||
}), 400
|
||||
|
||||
# 查询赛道关联信息
|
||||
track_query = text("""
|
||||
SELECT DISTINCT pc.stock_code, ci.belong_industry
|
||||
FROM gp_product_category pc
|
||||
JOIN gp_category_industry ci ON pc.category_name = ci.category_name
|
||||
WHERE pc.stock_code = :stock_code
|
||||
""")
|
||||
|
||||
# 获取赛道信息
|
||||
tracks = []
|
||||
try:
|
||||
# 获取数据库连接
|
||||
db_session = next(get_db())
|
||||
# 执行查询
|
||||
result = db_session.execute(track_query, {"stock_code": stock_code})
|
||||
for row in result:
|
||||
if row.belong_industry: # 确保不为空
|
||||
tracks.append(row.belong_industry)
|
||||
except Exception as e:
|
||||
logger.error(f"查询赛道信息失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"查询赛道信息失败: {str(e)}"
|
||||
}), 500
|
||||
finally:
|
||||
if 'db_session' in locals() and db_session is not None:
|
||||
db_session.close() # 关闭会话
|
||||
|
||||
# 返回结果
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"stock_code": stock_code,
|
||||
"tracks": tracks
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取股票赛道信息异常: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"服务器错误: {str(e)}"
|
||||
}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
|
@ -0,0 +1,595 @@
|
|||
/**
|
||||
* 沪深港通资金流向监控JS
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化图表
|
||||
let northChart = null;
|
||||
let southChart = null;
|
||||
|
||||
// 初始化图表函数,确保DOM元素存在
|
||||
function initCharts() {
|
||||
try {
|
||||
const northChartDom = document.getElementById('northChart');
|
||||
const southChartDom = document.getElementById('southChart');
|
||||
|
||||
if (northChartDom && !northChart) {
|
||||
try {
|
||||
northChart = echarts.init(northChartDom);
|
||||
console.log('北向资金图表初始化成功');
|
||||
} catch (e) {
|
||||
console.error('北向资金图表初始化失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (southChartDom && !southChart) {
|
||||
try {
|
||||
southChart = echarts.init(southChartDom);
|
||||
console.log('南向资金图表初始化成功');
|
||||
} catch (e) {
|
||||
console.error('南向资金图表初始化失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return northChart && southChart;
|
||||
} catch (e) {
|
||||
console.error('图表初始化过程中发生错误:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保DOM加载完毕后初始化图表
|
||||
// 使用setTimeout确保DOM元素完全渲染
|
||||
setTimeout(function() {
|
||||
if (!initCharts()) {
|
||||
console.log('首次初始化图表不成功,将在数据加载时再次尝试');
|
||||
}
|
||||
|
||||
// 开始加载数据
|
||||
loadData();
|
||||
|
||||
// 设置自动刷新 (每分钟刷新一次)
|
||||
setInterval(loadData, 60000);
|
||||
}, 100);
|
||||
|
||||
// 设置图表自适应
|
||||
window.addEventListener('resize', function() {
|
||||
if (northChart) {
|
||||
try {
|
||||
northChart.resize();
|
||||
} catch (e) {
|
||||
console.error('北向资金图表调整大小失败:', e);
|
||||
}
|
||||
}
|
||||
if (southChart) {
|
||||
try {
|
||||
southChart.resize();
|
||||
} catch (e) {
|
||||
console.error('南向资金图表调整大小失败:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 刷新按钮事件
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
loadData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载北向和南向资金数据
|
||||
*/
|
||||
function loadData() {
|
||||
// 确保图表已经初始化
|
||||
initCharts();
|
||||
|
||||
// 显示加载中状态
|
||||
showLoading(true);
|
||||
|
||||
console.log('开始加载北向资金数据...');
|
||||
|
||||
// 获取北向资金数据 (从香港流入A股的资金)
|
||||
fetch('/api/hsgt/northbound')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.message || '获取北向资金数据失败');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('北向资金数据获取成功:', data.status);
|
||||
|
||||
if (data.status === 'success' && data.data && data.data.success) {
|
||||
// 检查数据结构
|
||||
console.log('北向资金数据结构:', {
|
||||
hasTimeArray: Array.isArray(data.data.times),
|
||||
timeLength: data.data.times ? data.data.times.length : 0,
|
||||
hasDataObj: !!data.data.data,
|
||||
totalLength: data.data.data && data.data.data.total ? data.data.data.total.length : 0,
|
||||
hasCurrent: !!data.data.current
|
||||
});
|
||||
|
||||
// 渲染北向资金数据
|
||||
renderNorthboundData(data.data);
|
||||
} else {
|
||||
const errorMessage = data.data && data.data.message
|
||||
? data.data.message
|
||||
: (data.message || '北向资金数据格式错误');
|
||||
showError('北向资金数据获取失败: ' + errorMessage);
|
||||
|
||||
// 显示空数据状态
|
||||
renderEmptyNorthboundChart();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showError('请求北向资金数据错误: ' + error.message);
|
||||
console.error('北向资金数据请求异常:', error);
|
||||
// 显示空数据状态
|
||||
renderEmptyNorthboundChart();
|
||||
})
|
||||
.finally(() => {
|
||||
// 无论成功失败,都开始请求南向资金数据
|
||||
loadSouthboundData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染空的北向资金图表
|
||||
*/
|
||||
function renderEmptyNorthboundChart() {
|
||||
if (!northChart) return;
|
||||
|
||||
// 更新统计卡片为暂无数据
|
||||
updateStatCard('northTotal', null, '暂无数据');
|
||||
updateStatCard('northSH', null, '暂无数据');
|
||||
updateStatCard('northSZ', null, '暂无数据');
|
||||
|
||||
// 初始化一个简单的图表提示暂无数据
|
||||
northChart.setOption({
|
||||
title: {
|
||||
text: '陆股通资金流向(北向)',
|
||||
left: 'center',
|
||||
top: 0
|
||||
},
|
||||
graphic: {
|
||||
elements: [{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
style: {
|
||||
text: '暂无数据',
|
||||
fontSize: 20,
|
||||
fill: '#999'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载南向资金数据
|
||||
*/
|
||||
function loadSouthboundData() {
|
||||
console.log('开始加载南向资金数据...');
|
||||
|
||||
fetch('/api/hsgt/southbound')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.message || '获取南向资金数据失败');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('南向资金数据获取成功:', data.status);
|
||||
|
||||
if (data.status === 'success' && data.data && data.data.success) {
|
||||
// 检查数据结构
|
||||
console.log('南向资金数据结构:', {
|
||||
hasTimeArray: Array.isArray(data.data.times),
|
||||
timeLength: data.data.times ? data.data.times.length : 0,
|
||||
hasDataObj: !!data.data.data,
|
||||
totalLength: data.data.data && data.data.data.total ? data.data.data.total.length : 0,
|
||||
hasCurrent: !!data.data.current
|
||||
});
|
||||
|
||||
// 渲染南向资金数据
|
||||
renderSouthboundData(data.data);
|
||||
} else {
|
||||
const errorMessage = data.data && data.data.message
|
||||
? data.data.message
|
||||
: (data.message || '南向资金数据格式错误');
|
||||
showError('南向资金数据获取失败: ' + errorMessage);
|
||||
|
||||
// 显示空数据状态
|
||||
renderEmptySouthboundChart();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showError('请求南向资金数据错误: ' + error.message);
|
||||
console.error('南向资金数据请求异常:', error);
|
||||
// 显示空数据状态
|
||||
renderEmptySouthboundChart();
|
||||
})
|
||||
.finally(() => {
|
||||
// 隐藏加载中状态
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染空的南向资金图表
|
||||
*/
|
||||
function renderEmptySouthboundChart() {
|
||||
if (!southChart) return;
|
||||
|
||||
// 更新统计卡片为暂无数据
|
||||
updateStatCard('southTotal', null, '暂无数据');
|
||||
updateStatCard('southHKSH', null, '暂无数据');
|
||||
updateStatCard('southHKSZ', null, '暂无数据');
|
||||
|
||||
// 初始化一个简单的图表提示暂无数据
|
||||
southChart.setOption({
|
||||
title: {
|
||||
text: '港股通资金流向(南向)',
|
||||
left: 'center',
|
||||
top: 0
|
||||
},
|
||||
graphic: {
|
||||
elements: [{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
style: {
|
||||
text: '暂无数据',
|
||||
fontSize: 20,
|
||||
fill: '#999'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染北向资金数据 (从香港流入A股的资金)
|
||||
*/
|
||||
function renderNorthboundData(data) {
|
||||
if (!northChart) return;
|
||||
|
||||
try {
|
||||
// 验证数据有效性
|
||||
if (!data || !data.data || !data.times || !data.current) {
|
||||
renderEmptyNorthboundChart();
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备简化的图表数据
|
||||
const times = data.times;
|
||||
const seriesData = [];
|
||||
|
||||
// 处理北向资金总量
|
||||
if (data.data.total && Array.isArray(data.data.total) && data.data.total.length > 0) {
|
||||
seriesData.push({
|
||||
name: '北向资金',
|
||||
type: 'line',
|
||||
data: data.data.total,
|
||||
lineStyle: {width: 3}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理沪股通
|
||||
if (data.data.sh && Array.isArray(data.data.sh) && data.data.sh.length > 0) {
|
||||
seriesData.push({
|
||||
name: '沪股通',
|
||||
type: 'line',
|
||||
data: data.data.sh
|
||||
});
|
||||
}
|
||||
|
||||
// 处理深股通
|
||||
if (data.data.sz && Array.isArray(data.data.sz) && data.data.sz.length > 0) {
|
||||
seriesData.push({
|
||||
name: '深股通',
|
||||
type: 'line',
|
||||
data: data.data.sz
|
||||
});
|
||||
}
|
||||
|
||||
// 如果没有有效的系列数据,显示空图表
|
||||
if (seriesData.length === 0) {
|
||||
renderEmptyNorthboundChart();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新统计卡片
|
||||
updateStatCard('northTotal', data.current.total);
|
||||
updateStatCard('northSH', data.current.sh);
|
||||
updateStatCard('northSZ', data.current.sz);
|
||||
|
||||
// 更新时间
|
||||
const updateTimeElem = document.getElementById('updateTime');
|
||||
if (updateTimeElem) {
|
||||
updateTimeElem.textContent = '最后更新时间: ' + data.update_time;
|
||||
}
|
||||
|
||||
// 创建简单的图表配置
|
||||
const option = {
|
||||
title: {
|
||||
text: '陆股通资金流向(北向)',
|
||||
left: 'center',
|
||||
top: 0
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
if (!params || params.length === 0) return '';
|
||||
|
||||
const time = params[0].axisValue;
|
||||
let result = `${time}<br/>`;
|
||||
|
||||
params.forEach(param => {
|
||||
if (param && param.value !== undefined) {
|
||||
const value = param.value;
|
||||
const color = param.color;
|
||||
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
let valueText = value !== null ? value.toFixed(2) : 'N/A';
|
||||
if (value > 0) valueText = '+' + valueText;
|
||||
result += `${marker}${param.seriesName}: ${valueText} 亿<br/>`;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: seriesData.map(item => item.name),
|
||||
top: 30
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: 80,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: times,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '净流入金额(亿元)',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return value.toFixed(1);
|
||||
}
|
||||
}
|
||||
},
|
||||
series: seriesData,
|
||||
color: ['#ec0000', '#1e88e5', '#ff9800']
|
||||
};
|
||||
|
||||
// 安全地设置图表
|
||||
if (northChart && northChart.setOption) {
|
||||
northChart.setOption(option, true);
|
||||
} else {
|
||||
console.error('北向资金图表实例无效');
|
||||
const northChartDom = document.getElementById('northChart');
|
||||
if (northChartDom) {
|
||||
northChart = echarts.init(northChartDom);
|
||||
northChart.setOption(option, true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置北向资金图表选项时出错:', error);
|
||||
renderEmptyNorthboundChart();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染南向资金数据 (从内地流入港股的资金)
|
||||
*/
|
||||
function renderSouthboundData(data) {
|
||||
if (!southChart) return;
|
||||
|
||||
try {
|
||||
// 验证数据有效性
|
||||
if (!data || !data.data || !data.times || !data.current) {
|
||||
renderEmptySouthboundChart();
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备简化的图表数据
|
||||
const times = data.times;
|
||||
const seriesData = [];
|
||||
|
||||
// 处理南向资金总量
|
||||
if (data.data.total && Array.isArray(data.data.total) && data.data.total.length > 0) {
|
||||
seriesData.push({
|
||||
name: '南向资金',
|
||||
type: 'line',
|
||||
data: data.data.total,
|
||||
lineStyle: {width: 3}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理沪市港股通
|
||||
if (data.data.hk_sh && Array.isArray(data.data.hk_sh) && data.data.hk_sh.length > 0) {
|
||||
seriesData.push({
|
||||
name: '沪市港股通',
|
||||
type: 'line',
|
||||
data: data.data.hk_sh
|
||||
});
|
||||
}
|
||||
|
||||
// 处理深市港股通
|
||||
if (data.data.hk_sz && Array.isArray(data.data.hk_sz) && data.data.hk_sz.length > 0) {
|
||||
seriesData.push({
|
||||
name: '深市港股通',
|
||||
type: 'line',
|
||||
data: data.data.hk_sz
|
||||
});
|
||||
}
|
||||
|
||||
// 如果没有有效的系列数据,显示空图表
|
||||
if (seriesData.length === 0) {
|
||||
renderEmptySouthboundChart();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新统计卡片
|
||||
updateStatCard('southTotal', data.current.total);
|
||||
updateStatCard('southHKSH', data.current.hk_sh);
|
||||
updateStatCard('southHKSZ', data.current.hk_sz);
|
||||
|
||||
// 创建简单的图表配置
|
||||
const option = {
|
||||
title: {
|
||||
text: '港股通资金流向(南向)',
|
||||
left: 'center',
|
||||
top: 0
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
if (!params || params.length === 0) return '';
|
||||
|
||||
const time = params[0].axisValue;
|
||||
let result = `${time}<br/>`;
|
||||
|
||||
params.forEach(param => {
|
||||
if (param && param.value !== undefined) {
|
||||
const value = param.value;
|
||||
const color = param.color;
|
||||
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
let valueText = value !== null ? value.toFixed(2) : 'N/A';
|
||||
if (value > 0) valueText = '+' + valueText;
|
||||
result += `${marker}${param.seriesName}: ${valueText} 亿<br/>`;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: seriesData.map(item => item.name),
|
||||
top: 30
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: 80,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: times,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '净流入金额(亿元)',
|
||||
axisLabel: {
|
||||
formatter: function(value) {
|
||||
return value.toFixed(1);
|
||||
}
|
||||
}
|
||||
},
|
||||
series: seriesData,
|
||||
color: ['#ec0000', '#1e88e5', '#ff9800']
|
||||
};
|
||||
|
||||
// 安全地设置图表
|
||||
if (southChart && southChart.setOption) {
|
||||
southChart.setOption(option, true);
|
||||
} else {
|
||||
console.error('南向资金图表实例无效');
|
||||
const southChartDom = document.getElementById('southChart');
|
||||
if (southChartDom) {
|
||||
southChart = echarts.init(southChartDom);
|
||||
southChart.setOption(option, true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置南向资金图表选项时出错:', error);
|
||||
renderEmptySouthboundChart();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计卡片
|
||||
*/
|
||||
function updateStatCard(id, value, customText) {
|
||||
const statCard = document.getElementById(id);
|
||||
if (!statCard) return;
|
||||
|
||||
const statValue = statCard.querySelector('.stat-value');
|
||||
if (!statValue) return;
|
||||
|
||||
if (customText) {
|
||||
statValue.textContent = customText;
|
||||
statValue.classList.remove('money-inflow', 'money-outflow');
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
statValue.textContent = '--';
|
||||
statValue.classList.remove('money-inflow', 'money-outflow');
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化显示,保留两位小数
|
||||
const formattedValue = value.toFixed(2);
|
||||
|
||||
// 根据正负值设置样式
|
||||
if (value > 0) {
|
||||
statValue.textContent = '+' + formattedValue;
|
||||
statValue.classList.add('money-inflow');
|
||||
statValue.classList.remove('money-outflow');
|
||||
} else if (value < 0) {
|
||||
statValue.textContent = formattedValue;
|
||||
statValue.classList.add('money-outflow');
|
||||
statValue.classList.remove('money-inflow');
|
||||
} else {
|
||||
statValue.textContent = formattedValue;
|
||||
statValue.classList.remove('money-inflow', 'money-outflow');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息
|
||||
*/
|
||||
function showError(message) {
|
||||
console.error(message);
|
||||
// 可以添加Toast或其他UI提示
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示/隐藏加载状态
|
||||
*/
|
||||
function showLoading(isLoading) {
|
||||
if (!northChart || !southChart) return;
|
||||
|
||||
// 实现加载中状态显示
|
||||
if (isLoading) {
|
||||
northChart.showLoading({text: '加载中...'});
|
||||
southChart.showLoading({text: '加载中...'});
|
||||
} else {
|
||||
northChart.hideLoading();
|
||||
southChart.hideLoading();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,765 @@
|
|||
/**
|
||||
* 行业估值分析工具前端JS
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 获取DOM元素
|
||||
const industryForm = document.getElementById('industryForm');
|
||||
const industryNameSelect = document.getElementById('industryName');
|
||||
const startDateInput = document.getElementById('startDate');
|
||||
const metricSelect = document.getElementById('metric');
|
||||
const showCrowdingCheckbox = document.getElementById('showCrowding');
|
||||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const loadingSpinner = document.getElementById('loadingSpinner');
|
||||
const resultCard = document.getElementById('resultCard');
|
||||
const resultTitle = document.getElementById('resultTitle');
|
||||
const errorAlert = document.getElementById('errorAlert');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const percentileTable = document.getElementById('percentileTable');
|
||||
const crowdingStats = document.getElementById('crowdingStats');
|
||||
const crowdingTable = document.getElementById('crowdingTable');
|
||||
const industryStatsTable = document.getElementById('industryStatsTable');
|
||||
const crowdingChartRow = document.getElementById('crowdingChartRow');
|
||||
|
||||
// 定义图表实例
|
||||
let valuationChart = null;
|
||||
let crowdingChart = null;
|
||||
|
||||
// 初始化 - 加载行业列表
|
||||
loadIndustryList();
|
||||
|
||||
// 监听表单提交事件
|
||||
industryForm.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
analyzeIndustry();
|
||||
});
|
||||
|
||||
// 监听重置按钮点击事件
|
||||
resetBtn.addEventListener('click', function() {
|
||||
resetForm();
|
||||
});
|
||||
|
||||
// 拥挤度显示控制
|
||||
showCrowdingCheckbox.addEventListener('change', function() {
|
||||
if (crowdingStats.classList.contains('d-none')) {
|
||||
// 如果还没有数据,不做任何操作
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.checked) {
|
||||
// 显示拥挤度
|
||||
crowdingStats.classList.remove('d-none');
|
||||
crowdingChartRow.classList.remove('d-none');
|
||||
} else {
|
||||
// 隐藏拥挤度
|
||||
crowdingStats.classList.add('d-none');
|
||||
crowdingChartRow.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载行业列表
|
||||
*/
|
||||
function loadIndustryList() {
|
||||
showLoading(true);
|
||||
|
||||
fetch('/api/industry/list')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.message || '请求失败');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// 填充行业下拉列表
|
||||
populateIndustrySelect(data.data);
|
||||
} else {
|
||||
showError(data.message || '获取行业列表失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showError(error.message || '请求失败,请检查网络连接');
|
||||
})
|
||||
.finally(() => {
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充行业下拉列表
|
||||
*/
|
||||
function populateIndustrySelect(industries) {
|
||||
// 清空选项(保留第一个默认选项)
|
||||
industryNameSelect.innerHTML = '<option value="" selected disabled>请选择行业</option>';
|
||||
|
||||
// 排序行业列表(按名称)
|
||||
industries.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
|
||||
|
||||
// 添加行业选项
|
||||
industries.forEach(industry => {
|
||||
const option = document.createElement('option');
|
||||
option.value = industry.name;
|
||||
option.textContent = industry.name;
|
||||
industryNameSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析行业估值
|
||||
*/
|
||||
function analyzeIndustry() {
|
||||
// 显示加载中状态
|
||||
showLoading(true);
|
||||
|
||||
// 隐藏之前的结果和错误信息
|
||||
resultCard.classList.add('d-none');
|
||||
errorAlert.classList.add('d-none');
|
||||
|
||||
// 获取表单数据
|
||||
const industryName = industryNameSelect.value;
|
||||
const startDate = startDateInput.value;
|
||||
const metric = metricSelect.value;
|
||||
|
||||
// 构建请求URL
|
||||
let url = `/api/industry/analysis?industry_name=${encodeURIComponent(industryName)}&metric=${metric}`;
|
||||
|
||||
if (startDate) {
|
||||
url += `&start_date=${startDate}`;
|
||||
}
|
||||
|
||||
// 发送API请求
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.message || '请求失败');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// 渲染分析结果
|
||||
renderIndustryResults(data);
|
||||
} else {
|
||||
showError(data.message || '分析失败,请稍后再试');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showError(error.message || '请求失败,请检查网络连接');
|
||||
})
|
||||
.finally(() => {
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染行业分析结果
|
||||
*/
|
||||
function renderIndustryResults(data) {
|
||||
// 显示结果卡片(在渲染图表前确保容器可见)
|
||||
resultCard.classList.remove('d-none');
|
||||
|
||||
// 更新标题
|
||||
resultTitle.textContent = data.data.title.text;
|
||||
|
||||
// 渲染分位数数据表格
|
||||
renderPercentileTable(data.data.percentiles, data.data.yAxis[0].name);
|
||||
|
||||
// 渲染行业基本统计
|
||||
renderIndustryStats(data.data.percentiles);
|
||||
|
||||
// 渲染拥挤度数据(如果有)
|
||||
if (data.data.crowding) {
|
||||
renderCrowdingStats(data.data.crowding);
|
||||
|
||||
// 根据复选框状态决定是否显示拥挤度
|
||||
if (showCrowdingCheckbox.checked) {
|
||||
crowdingStats.classList.remove('d-none');
|
||||
crowdingChartRow.classList.remove('d-none');
|
||||
} else {
|
||||
crowdingStats.classList.add('d-none');
|
||||
crowdingChartRow.classList.add('d-none');
|
||||
}
|
||||
} else {
|
||||
crowdingStats.classList.add('d-none');
|
||||
crowdingChartRow.classList.add('d-none');
|
||||
}
|
||||
|
||||
// 渲染图表(等待DOM更新后)
|
||||
setTimeout(() => {
|
||||
renderValuationChart(data.data);
|
||||
|
||||
// 如果有拥挤度数据且复选框被选中,渲染拥挤度图表
|
||||
if (data.data.crowding && showCrowdingCheckbox.checked) {
|
||||
renderCrowdingChart(data.data);
|
||||
}
|
||||
|
||||
// 滚动到结果区域
|
||||
resultCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染估值分位数ECharts图表
|
||||
*/
|
||||
function renderValuationChart(chartData) {
|
||||
// 初始化图表
|
||||
const chartContainer = document.getElementById('valuationChart');
|
||||
|
||||
// 确保容器可见
|
||||
resultCard.classList.remove('d-none');
|
||||
|
||||
// 如果已经存在图表实例,则销毁
|
||||
if (valuationChart) {
|
||||
valuationChart.dispose();
|
||||
}
|
||||
|
||||
// 创建新的图表实例
|
||||
valuationChart = echarts.init(chartContainer);
|
||||
|
||||
// 处理图例数据
|
||||
let legendData = chartData.legend.data.filter(item => !item.includes('拥挤度'));
|
||||
|
||||
// 处理系列数据
|
||||
let seriesData = chartData.series.filter(series =>
|
||||
!series.name.includes('拥挤度') && series.yAxisIndex !== 1
|
||||
);
|
||||
|
||||
// 去掉所有折线上的圆圈标记
|
||||
seriesData.forEach(series => {
|
||||
series.symbol = 'none'; // 不显示折线上的圆点标记
|
||||
});
|
||||
|
||||
// 检查数据有效性
|
||||
// 注意:现在所有图例都是存在的,因为我们使用行业平均值的历史统计生成它们
|
||||
// 不再需要检查q1_values, q3_values等是否存在
|
||||
|
||||
// 计算Y轴范围
|
||||
const percentiles = chartData.percentiles;
|
||||
const avgValues = chartData.valuation.avg_values.filter(v => v !== null && v !== undefined);
|
||||
|
||||
// 找出数据的最小值和最大值
|
||||
let minValue = percentiles.min;
|
||||
let maxValue = percentiles.max;
|
||||
|
||||
// 如果有avgValues,也考虑其最小值和最大值
|
||||
if (avgValues.length > 0) {
|
||||
const avgMin = Math.min(...avgValues);
|
||||
const avgMax = Math.max(...avgValues);
|
||||
minValue = Math.min(minValue, avgMin);
|
||||
maxValue = Math.max(maxValue, avgMax);
|
||||
}
|
||||
|
||||
// 计算Y轴范围,给出更好的视觉展示
|
||||
// 下限:取最小值的80%或最小值减去范围的20%,但不小于0
|
||||
// 上限:取最大值的120%或最大值加上范围的20%
|
||||
const range = maxValue - minValue;
|
||||
const padding = range * 0.2;
|
||||
let yMinValue = Math.max(0, minValue - padding);
|
||||
let yMaxValue = maxValue + padding;
|
||||
|
||||
// 如果最小值与最大值之间的差距太小,进一步扩大范围以便更好地展示
|
||||
if (range / maxValue < 0.1) { // 如果范围小于最大值的10%
|
||||
yMinValue = Math.max(0, minValue * 0.5); // 下限可以更低,但不低于0
|
||||
yMaxValue = maxValue * 1.3; // 上限可以更高
|
||||
}
|
||||
|
||||
// 设置图表选项
|
||||
const option = {
|
||||
title: {
|
||||
text: chartData.title.text,
|
||||
subtext: chartData.title.subtext,
|
||||
left: 'center',
|
||||
top: 5
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: function(params) {
|
||||
const date = params[0].axisValue;
|
||||
let result = `${date}<br/>`;
|
||||
|
||||
params.forEach(param => {
|
||||
const value = param.value;
|
||||
const color = param.color;
|
||||
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
const name = param.seriesName;
|
||||
|
||||
if (name.includes('历史')) {
|
||||
// 对于历史统计线,使用固定值显示
|
||||
result += `${marker}${name}: ${value !== null ? value.toFixed(2) : 'N/A'}<br/>`;
|
||||
} else {
|
||||
// 对于当前值,显示更多小数位
|
||||
result += `${marker}${name}: ${value !== null ? value.toFixed(2) : 'N/A'}<br/>`;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: legendData,
|
||||
top: 60
|
||||
},
|
||||
grid: chartData.grid[0],
|
||||
toolbox: chartData.toolbox,
|
||||
xAxis: chartData.xAxis[0],
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: chartData.yAxis[0].name,
|
||||
min: yMinValue,
|
||||
max: yMaxValue,
|
||||
axisLabel: {
|
||||
formatter: function(value) { return value.toFixed(1); }
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
xAxisIndex: [0]
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 100,
|
||||
xAxisIndex: [0]
|
||||
}
|
||||
],
|
||||
series: seriesData
|
||||
};
|
||||
|
||||
// 设置图表选项
|
||||
valuationChart.setOption(option);
|
||||
|
||||
// 手动触发一次resize,确保图表正确渲染
|
||||
setTimeout(() => {
|
||||
valuationChart.resize();
|
||||
}, 50);
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', function() {
|
||||
if (valuationChart) {
|
||||
valuationChart.resize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染拥挤度ECharts图表
|
||||
*/
|
||||
function renderCrowdingChart(chartData) {
|
||||
// 初始化图表
|
||||
const chartContainer = document.getElementById('crowdingChart');
|
||||
|
||||
// 确保容器可见
|
||||
crowdingChartRow.classList.remove('d-none');
|
||||
|
||||
// 如果已经存在图表实例,则销毁
|
||||
if (crowdingChart) {
|
||||
crowdingChart.dispose();
|
||||
}
|
||||
|
||||
// 创建新的图表实例
|
||||
crowdingChart = echarts.init(chartContainer);
|
||||
|
||||
// 使用拥挤度的完整数据,不受其他时间筛选影响
|
||||
// 拥挤度数据在后端已经固定为3年数据
|
||||
const crowdingDates = chartData.crowding.dates;
|
||||
const crowdingPercentiles = chartData.crowding.percentiles;
|
||||
|
||||
// 筛选拥挤度相关的系列
|
||||
const crowdingSeries = {
|
||||
name: '行业交易拥挤度历史百分位',
|
||||
type: 'line',
|
||||
data: crowdingPercentiles,
|
||||
symbol: 'none', // 不显示折线上的圆点标记
|
||||
lineStyle: {width: 2, color: '#ff7f50'},
|
||||
areaStyle: {opacity: 0.2, color: '#ff7f50'},
|
||||
markLine: {
|
||||
data: [
|
||||
{name: "20%", yAxis: 20, lineStyle: {color: "#28a745", type: "dashed"}},
|
||||
{name: "80%", yAxis: 80, lineStyle: {color: "#dc3545", type: "dashed"}}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// 获取拥挤度数据,用于计算Y轴范围
|
||||
// 使用百分位数值替代原始比例
|
||||
let percentileValues = crowdingPercentiles.filter(v => v !== null && v !== undefined);
|
||||
|
||||
// 计算Y轴范围
|
||||
let yMinValue = 0; // 默认最小值为0
|
||||
let yMaxValue = 100; // 默认最大值为100(百分比)
|
||||
|
||||
// 如果有实际数据,可以根据数据调整范围
|
||||
if (percentileValues.length > 0) {
|
||||
const minPercentile = Math.min(...percentileValues);
|
||||
const maxPercentile = Math.max(...percentileValues);
|
||||
|
||||
// 但拥挤度图表通常应该保持0-100的范围,所以只在必要时调整
|
||||
if (minPercentile > 20 && maxPercentile < 80) {
|
||||
// 如果数据都集中在20-80之间,可以适当缩小范围
|
||||
yMinValue = Math.max(0, Math.floor(minPercentile / 10) * 10 - 10); // 向下取整到最接近的10的倍数再减10
|
||||
yMaxValue = Math.min(100, Math.ceil(maxPercentile / 10) * 10 + 10); // 向上取整到最接近的10的倍数再加10
|
||||
}
|
||||
}
|
||||
|
||||
// 设置图表选项
|
||||
const option = {
|
||||
title: {
|
||||
text: '行业交易拥挤度分析',
|
||||
subtext: `当前拥挤度历史百分位: ${chartData.crowding.current_percentile.toFixed(2)}%`,
|
||||
left: 'center',
|
||||
top: 5
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
},
|
||||
formatter: function(params) {
|
||||
const date = params[0].axisValue;
|
||||
let result = `${date}<br/>`;
|
||||
|
||||
params.forEach(param => {
|
||||
const value = param.value;
|
||||
const color = param.color;
|
||||
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
|
||||
result += `${marker}${param.seriesName}: ${value ? value.toFixed(2) : 'N/A'}%<br/>`;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['行业交易拥挤度历史百分位'],
|
||||
top: 60
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '100',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: crowdingDates,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '历史百分位 (%)',
|
||||
min: yMinValue,
|
||||
max: yMaxValue,
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: function(value) { return value.toFixed(1) + '%'; }
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
series: [crowdingSeries],
|
||||
visualMap: {
|
||||
show: false,
|
||||
dimension: 1,
|
||||
pieces: [
|
||||
{min: 0, max: 20, color: '#91cc75'}, // 绿色 - 不拥挤
|
||||
{min: 20, max: 40, color: '#91cc75'}, // 绿色 - 较不拥挤
|
||||
{min: 40, max: 60, color: '#fac858'}, // 黄色 - 中性
|
||||
{min: 60, max: 80, color: '#ee6666'}, // 红色 - 较为拥挤
|
||||
{min: 80, max: 100, color: '#ee6666'} // 红色 - 极度拥挤
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// 设置图表选项
|
||||
crowdingChart.setOption(option);
|
||||
|
||||
// 手动触发一次resize,确保图表正确渲染
|
||||
setTimeout(() => {
|
||||
crowdingChart.resize();
|
||||
}, 50);
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', function() {
|
||||
if (crowdingChart) {
|
||||
crowdingChart.resize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染分位数表格
|
||||
*/
|
||||
function renderPercentileTable(percentiles, metricName) {
|
||||
// 清空表格
|
||||
percentileTable.innerHTML = '';
|
||||
|
||||
// 格式化函数
|
||||
const formatValue = value => value ? value.toFixed(2) : 'N/A';
|
||||
|
||||
// 获取百分位类别
|
||||
const getPercentileClass = value => {
|
||||
if (value <= 33) return 'percent-low';
|
||||
if (value <= 66) return 'percent-medium';
|
||||
return 'percent-high';
|
||||
};
|
||||
|
||||
// 创建表格行
|
||||
const rows = [
|
||||
{
|
||||
label: '当前行业均值',
|
||||
value: formatValue(percentiles.current),
|
||||
class: 'current-value' // 添加特殊样式类
|
||||
},
|
||||
{
|
||||
label: '历史百分位',
|
||||
value: `${formatValue(percentiles.percentile)}%`,
|
||||
class: getPercentileClass(percentiles.percentile)
|
||||
},
|
||||
{
|
||||
label: '历史最小值',
|
||||
value: formatValue(percentiles.min),
|
||||
class: 'percent-low' // 绿色
|
||||
},
|
||||
{
|
||||
label: '历史最大值',
|
||||
value: formatValue(percentiles.max),
|
||||
class: 'percent-high' // 红色
|
||||
},
|
||||
{
|
||||
label: '历史均值',
|
||||
value: formatValue(percentiles.mean)
|
||||
},
|
||||
{
|
||||
label: '历史中位数',
|
||||
value: formatValue(percentiles.median)
|
||||
},
|
||||
{
|
||||
label: '历史第一四分位',
|
||||
value: formatValue(percentiles.q1),
|
||||
class: 'percent-low' // 绿色
|
||||
},
|
||||
{
|
||||
label: '历史第三四分位',
|
||||
value: formatValue(percentiles.q3),
|
||||
class: 'percent-high' // 红色
|
||||
}
|
||||
];
|
||||
|
||||
// 添加行到表格
|
||||
rows.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// 添加特殊类以突出当前值行
|
||||
if (row.label === '当前行业均值' || row.label === '历史百分位') {
|
||||
tr.classList.add('highlight-row');
|
||||
}
|
||||
|
||||
const tdLabel = document.createElement('td');
|
||||
tdLabel.textContent = row.label;
|
||||
tr.appendChild(tdLabel);
|
||||
|
||||
const tdValue = document.createElement('td');
|
||||
tdValue.textContent = row.value;
|
||||
|
||||
if (row.class) {
|
||||
tdValue.classList.add('percent-value', row.class);
|
||||
}
|
||||
|
||||
tr.appendChild(tdValue);
|
||||
percentileTable.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染行业统计数据
|
||||
*/
|
||||
function renderIndustryStats(percentiles) {
|
||||
// 清空表格
|
||||
industryStatsTable.innerHTML = '';
|
||||
|
||||
// 获取用户选择的开始日期,如果没有选择则显示"最近三年"
|
||||
const userStartDate = startDateInput.value ? startDateInput.value : "最近三年";
|
||||
|
||||
// 创建表格行
|
||||
const rows = [
|
||||
{ label: '行业成份股数量', value: percentiles.stock_count + '只' },
|
||||
{ label: '分析开始日期', value: userStartDate }
|
||||
];
|
||||
|
||||
// 添加行到表格
|
||||
rows.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const tdLabel = document.createElement('td');
|
||||
tdLabel.textContent = row.label;
|
||||
tr.appendChild(tdLabel);
|
||||
|
||||
const tdValue = document.createElement('td');
|
||||
tdValue.textContent = row.value;
|
||||
tr.appendChild(tdValue);
|
||||
|
||||
industryStatsTable.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染拥挤度统计数据
|
||||
*/
|
||||
function renderCrowdingStats(crowding) {
|
||||
// 清空表格
|
||||
crowdingTable.innerHTML = '';
|
||||
|
||||
// 获取拥挤度级别的颜色类
|
||||
const getCrowdingLevelClass = level => {
|
||||
switch(level) {
|
||||
case '不拥挤':
|
||||
case '较不拥挤':
|
||||
return 'percent-low'; // 绿色
|
||||
case '中性':
|
||||
return 'percent-medium'; // 黄色
|
||||
case '较为拥挤':
|
||||
case '极度拥挤':
|
||||
return 'percent-high'; // 红色
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 创建表格行
|
||||
const rows = [
|
||||
{
|
||||
label: '当前成交占比',
|
||||
value: crowding.current_ratio.toFixed(2) + '%',
|
||||
tooltip: '行业成交额占全市场成交额的百分比'
|
||||
},
|
||||
{
|
||||
label: '历史百分位',
|
||||
value: crowding.current_percentile.toFixed(2) + '%',
|
||||
class: getCrowdingLevelClass(crowding.level),
|
||||
tooltip: '当前行业成交占比在历史分布中的位置'
|
||||
},
|
||||
{
|
||||
label: '拥挤度评级',
|
||||
value: crowding.level,
|
||||
class: getCrowdingLevelClass(crowding.level),
|
||||
tooltip: '根据历史百分位划分的拥挤度等级'
|
||||
}
|
||||
];
|
||||
|
||||
// 添加行到表格
|
||||
rows.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const tdLabel = document.createElement('td');
|
||||
tdLabel.textContent = row.label;
|
||||
|
||||
// 添加工具提示
|
||||
if (row.tooltip) {
|
||||
tdLabel.title = row.tooltip;
|
||||
tdLabel.style.cursor = 'help';
|
||||
}
|
||||
|
||||
tr.appendChild(tdLabel);
|
||||
|
||||
const tdValue = document.createElement('td');
|
||||
tdValue.textContent = row.value;
|
||||
|
||||
if (row.class) {
|
||||
tdValue.classList.add('percent-value', row.class);
|
||||
}
|
||||
|
||||
tr.appendChild(tdValue);
|
||||
crowdingTable.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息
|
||||
*/
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorAlert.classList.remove('d-none');
|
||||
|
||||
// 滚动到错误信息
|
||||
errorAlert.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
function resetForm() {
|
||||
industryForm.reset();
|
||||
|
||||
// 隐藏结果和错误信息
|
||||
resultCard.classList.add('d-none');
|
||||
errorAlert.classList.add('d-none');
|
||||
|
||||
// 销毁图表
|
||||
if (valuationChart) {
|
||||
valuationChart.dispose();
|
||||
valuationChart = null;
|
||||
}
|
||||
|
||||
if (crowdingChart) {
|
||||
crowdingChart.dispose();
|
||||
crowdingChart = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示/隐藏加载状态
|
||||
*/
|
||||
function showLoading(isLoading) {
|
||||
if (isLoading) {
|
||||
loadingSpinner.classList.remove('d-none');
|
||||
analyzeBtn.disabled = true;
|
||||
} else {
|
||||
loadingSpinner.classList.add('d-none');
|
||||
analyzeBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,170 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>沪深港通资金流向监控</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link rel="stylesheet" href="../static/css/bootstrap.min.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>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="text-center">沪深港通资金流向监控
|
||||
<button id="refreshBtn" class="btn btn-sm btn-outline-primary refresh-btn">
|
||||
<i class="bi bi-arrow-clockwise"></i> 刷新数据
|
||||
</button>
|
||||
</h2>
|
||||
<p class="text-center update-time" id="updateTime"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 北向资金卡片 -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
北向资金流向 (单位:亿元)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="flow-direction text-center">提示:该走势图为陆股通指数成分股大单资金流向,非北向资金,但具备一定参考价值。</p>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card" id="northTotal">
|
||||
<div class="stat-value">--</div>
|
||||
<div class="stat-title">北向资金净流入(亿元)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card" id="northSH">
|
||||
<div class="stat-value">--</div>
|
||||
<div class="stat-title">沪股通净流入(亿元)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card" id="northSZ">
|
||||
<div class="stat-value">--</div>
|
||||
<div class="stat-title">深股通净流入(亿元)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="northChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 南向资金卡片 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
南向资金流向 (单位:亿元)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="flow-direction text-center">从内地流入港股的资金</p>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card" id="southTotal">
|
||||
<div class="stat-value">--</div>
|
||||
<div class="stat-title">南向资金净流入(亿元)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card" id="southHKSH">
|
||||
<div class="stat-value">--</div>
|
||||
<div class="stat-title">沪市港股通净流入(亿元)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card" id="southHKSZ">
|
||||
<div class="stat-value">--</div>
|
||||
<div class="stat-title">深市港股通净流入(亿元)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="southChart" class="chart-container"></div>
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript 库 -->
|
||||
<script src="../static/js/jquery.min.js"></script>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,272 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import pandas as pd
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class HSGTMonitor:
|
||||
"""沪深港通资金流向监控类"""
|
||||
|
||||
def __init__(self):
|
||||
# 初始化请求头
|
||||
self.headers = {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Host": "dataq.10jqka.com.cn",
|
||||
"Origin": "https://data.10jqka.com.cn",
|
||||
"Platform": "web",
|
||||
"Referer": "https://data.10jqka.com.cn/",
|
||||
"Source-Id": "b2cweb-hsgtconnect",
|
||||
"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\""
|
||||
}
|
||||
|
||||
# 请求URL
|
||||
self.url = "https://dataq.10jqka.com.cn/fetch-data-server/fetch/v1/interval_data"
|
||||
|
||||
# 北向资金代码映射 (从香港流入A股的资金)
|
||||
self.northbound_codes = {
|
||||
"total": "48:883957", # 陆股通整体(北向资金总和)
|
||||
"sh": "16:1A0001", # 沪股通
|
||||
"sz": "32:399001" # 深股通
|
||||
}
|
||||
|
||||
# 南向资金代码映射 (从内地流入港股的资金)
|
||||
self.southbound_codes = {
|
||||
"total": "48:883957", # 港股通整体(南向资金总和)
|
||||
"hk_sh": "16:1A0001", # 沪市港股通
|
||||
"hk_sz": "32:399001" # 深市港股通
|
||||
}
|
||||
|
||||
# 北向和南向资金的API参数
|
||||
self.index_ids = {
|
||||
"northbound": "hsgt_main_money", # 北向资金
|
||||
"southbound": "ggt_net_buy" # 南向资金
|
||||
}
|
||||
|
||||
def get_trading_hours_timestamp(self, date=None):
|
||||
"""获取指定日期的交易时段时间戳
|
||||
|
||||
交易时段: 9:30-11:30, 13:00-15:00
|
||||
为了完整捕捉开盘前的资金流向,开始时间设为9:20
|
||||
"""
|
||||
if date is None:
|
||||
date = datetime.now().date()
|
||||
elif isinstance(date, str):
|
||||
date = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
|
||||
# 创建当天交易开始和结束时间
|
||||
morning_start = datetime.combine(date, datetime.strptime("09:20", "%H:%M").time())
|
||||
trading_end = datetime.combine(date, datetime.strptime("15:00", "%H:%M").time())
|
||||
|
||||
# 检查当前是否已经过了交易结束时间
|
||||
current_time = datetime.now()
|
||||
if current_time > trading_end:
|
||||
end_time = trading_end
|
||||
else:
|
||||
end_time = current_time
|
||||
|
||||
# 转换为时间戳(秒)
|
||||
start_timestamp = int(morning_start.timestamp())
|
||||
end_timestamp = int(end_time.timestamp())
|
||||
|
||||
return start_timestamp, end_timestamp
|
||||
|
||||
def fetch_northbound_data(self, start_timestamp=None, end_timestamp=None):
|
||||
"""获取北向资金流向数据 (从香港流入A股的资金)"""
|
||||
return self._fetch_hsgt_data(
|
||||
"northbound",
|
||||
self.northbound_codes,
|
||||
self.index_ids["northbound"],
|
||||
start_timestamp,
|
||||
end_timestamp
|
||||
)
|
||||
|
||||
def fetch_southbound_data(self, start_timestamp=None, end_timestamp=None):
|
||||
"""获取南向资金流向数据 (从内地流入港股的资金)"""
|
||||
return self._fetch_hsgt_data(
|
||||
"southbound",
|
||||
self.southbound_codes,
|
||||
self.index_ids["southbound"],
|
||||
start_timestamp,
|
||||
end_timestamp
|
||||
)
|
||||
|
||||
def _fetch_hsgt_data(self, flow_type, code_map, index_id, start_timestamp=None, end_timestamp=None):
|
||||
"""通用的沪深港通资金流向数据获取方法
|
||||
|
||||
参数:
|
||||
flow_type: 资金流向类型,"northbound" 或 "southbound"
|
||||
code_map: 对应的代码映射
|
||||
index_id: API的index_id参数
|
||||
start_timestamp: 开始时间戳
|
||||
end_timestamp: 结束时间戳
|
||||
"""
|
||||
# 如果没有提供时间戳,使用当天的交易时段
|
||||
if start_timestamp is None or end_timestamp is None:
|
||||
start_timestamp, end_timestamp = self.get_trading_hours_timestamp()
|
||||
|
||||
# 获取代码列表
|
||||
if flow_type == "northbound":
|
||||
codes = [
|
||||
code_map["total"],
|
||||
code_map["sh"],
|
||||
code_map["sz"]
|
||||
]
|
||||
else: # southbound
|
||||
codes = [
|
||||
code_map["total"],
|
||||
code_map["hk_sh"],
|
||||
code_map["hk_sz"]
|
||||
]
|
||||
|
||||
# 构建请求体
|
||||
payload = {
|
||||
"indexes": [{
|
||||
"codes": codes,
|
||||
"index_info": [{
|
||||
"index_id": index_id
|
||||
}]
|
||||
}],
|
||||
"time_range": {
|
||||
"time_type": "TREND",
|
||||
"start": str(start_timestamp),
|
||||
"end": str(end_timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"请求{flow_type}资金数据: start={start_timestamp}, end={end_timestamp}, index_id={index_id}")
|
||||
|
||||
try:
|
||||
# 发送请求
|
||||
response = requests.post(self.url, headers=self.headers, json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
# 解析响应
|
||||
result = response.json()
|
||||
|
||||
if result["status_code"] == 0 and "data" in result:
|
||||
# 提取时间和各通道的数据
|
||||
time_range = [int(ts) for ts in result["data"]["time_range"]]
|
||||
time_formatted = [datetime.fromtimestamp(ts).strftime('%H:%M') for ts in time_range]
|
||||
|
||||
# 提取各通道的数据
|
||||
data_dict = {}
|
||||
|
||||
# 检查是否有数据
|
||||
if "data" in result["data"] and result["data"]["data"]:
|
||||
for item in result["data"]["data"]:
|
||||
code = item["code"]
|
||||
# 增加安全检查,确保values列表存在且不为空
|
||||
if "values" in item and len(item["values"]) > 0 and "values" in item["values"][0]:
|
||||
# 北向资金
|
||||
if flow_type == "northbound":
|
||||
if code == code_map["total"]:
|
||||
data_dict["total"] = item["values"][0]["values"]
|
||||
elif code == code_map["sh"]:
|
||||
data_dict["sh"] = item["values"][0]["values"]
|
||||
elif code == code_map["sz"]:
|
||||
data_dict["sz"] = item["values"][0]["values"]
|
||||
# 南向资金
|
||||
else:
|
||||
if code == code_map["total"]:
|
||||
data_dict["total"] = item["values"][0]["values"]
|
||||
elif code == code_map["hk_sh"]:
|
||||
data_dict["hk_sh"] = item["values"][0]["values"]
|
||||
elif code == code_map["hk_sz"]:
|
||||
data_dict["hk_sz"] = item["values"][0]["values"]
|
||||
|
||||
# 检查是否获取到了任何数据
|
||||
if not data_dict:
|
||||
logger.warning(f"{flow_type}资金数据响应中没有有效数据")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"{flow_type}资金数据响应中没有有效数据"
|
||||
}
|
||||
|
||||
# 计算当前值(增加更多的安全检查)
|
||||
current_values = {}
|
||||
|
||||
# 安全地获取当前值
|
||||
for key in data_dict:
|
||||
if data_dict[key] and len(data_dict[key]) > 0: # 确保数据列表不为空
|
||||
value = data_dict[key][-1]
|
||||
# 确保值不是None
|
||||
if value is not None:
|
||||
current_values[key] = value / 100000000
|
||||
else:
|
||||
current_values[key] = 0
|
||||
logger.warning(f"{flow_type}资金{key}最后一个值为None")
|
||||
else:
|
||||
current_values[key] = 0
|
||||
logger.warning(f"{flow_type}资金{key}数据为空")
|
||||
|
||||
# 转换为亿元单位,处理None值
|
||||
for key in data_dict:
|
||||
if data_dict[key]: # 确保数据列表不为空
|
||||
# 安全转换,处理None值
|
||||
data_dict[key] = [
|
||||
(value / 100000000 if value is not None else 0)
|
||||
for value in data_dict[key]
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"timestamps": time_range,
|
||||
"times": time_formatted,
|
||||
"data": data_dict,
|
||||
"current": current_values,
|
||||
"flow_type": flow_type,
|
||||
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
else:
|
||||
error_msg = result.get("status_msg", "Unknown error")
|
||||
logger.error(f"请求{flow_type}资金数据失败: {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"返回错误: {error_msg}"
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"请求{flow_type}资金数据异常: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"请求异常: {str(e)}"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"处理{flow_type}资金数据异常: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"处理异常: {str(e)}"
|
||||
}
|
||||
|
||||
# 测试代码
|
||||
if __name__ == "__main__":
|
||||
monitor = HSGTMonitor()
|
||||
|
||||
# 获取北向资金数据
|
||||
try:
|
||||
north_data = monitor.fetch_northbound_data()
|
||||
print("北向资金数据:")
|
||||
print(json.dumps(north_data, ensure_ascii=False, indent=2))
|
||||
except Exception as e:
|
||||
print(f"测试北向资金数据获取失败: {str(e)}")
|
||||
|
||||
# 获取南向资金数据
|
||||
try:
|
||||
south_data = monitor.fetch_southbound_data()
|
||||
print("\n南向资金数据:")
|
||||
print(json.dumps(south_data, ensure_ascii=False, indent=2))
|
||||
except Exception as e:
|
||||
print(f"测试南向资金数据获取失败: {str(e)}")
|
Loading…
Reference in New Issue