Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6a3f432
docs: 板块热力图功能设计 spec
henrylin99 Jun 7, 2026
9dec66d
docs: 板块热力图实施计划
henrylin99 Jun 7, 2026
3b29ce5
feat: add HeatmapService with sector aggregation logic
henrylin99 Jun 7, 2026
edd8868
feat: add heatmap route blueprint and nav link
henrylin99 Jun 7, 2026
3c5ea33
fix: use url_for in heatmap nav link for consistency
henrylin99 Jun 7, 2026
322ce76
feat: add heatmap template with ECharts treemap and stock table
henrylin99 Jun 7, 2026
20e9b31
feat: 热力图增加涨跌/市值排序切换,颜色阈值调整为±8%
henrylin99 Jun 7, 2026
64063e6
fix: 涨跌幅排序改变矩形面积而非仅数组顺序;开启 debug 模式
henrylin99 Jun 7, 2026
c178a73
fix: 板块颜色阈值调整为±6%达到最深
henrylin99 Jun 7, 2026
cdd7696
docs: add pattern screening feature design spec
henrylin99 Jun 7, 2026
ec3a83c
docs: address spec review issues - input validation, cache invalidati…
henrylin99 Jun 7, 2026
4e0ea56
docs: add pattern screen implementation plan (review-approved)
henrylin99 Jun 7, 2026
1ef9dea
feat: add PatternScreenService with metadata and AND-filtering
henrylin99 Jun 7, 2026
b404bba
feat: add pattern screen API endpoints (groups + screen)
henrylin99 Jun 7, 2026
376b1c9
feat: add pattern screening page with left panel + right table layout
henrylin99 Jun 7, 2026
0cb4218
fix: auto-screen on checkbox toggle, improve filter panel styling
henrylin99 Jun 7, 2026
473f7ac
fix: rewrite filter panel CSS to use Obsidian design tokens
henrylin99 Jun 7, 2026
7cf78ec
fix: force-override Bootstrap accordion and button light borders
henrylin99 Jun 7, 2026
de79215
fix: address PR review findings
henrylin99 Jun 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def create_app(config_name='default'):
from app.api.data_jobs_api import data_jobs_bp
from app.routes.ml_factor_routes import ml_factor_routes
from app.routes.realtime_analysis_routes import realtime_analysis_routes
from app.routes.heatmap import heatmap_routes
from app.routes.pattern_screen import pattern_screen_bp
from app.api.pattern_screen_api import pattern_screen_api
app.register_blueprint(api_bp, url_prefix='/api')
app.register_blueprint(ml_factor_bp)
app.register_blueprint(text2sql_bp)
Expand All @@ -50,7 +53,10 @@ def create_app(config_name='default'):
app.register_blueprint(data_jobs_bp)
app.register_blueprint(ml_factor_routes)
app.register_blueprint(realtime_analysis_routes)

app.register_blueprint(heatmap_routes)
app.register_blueprint(pattern_screen_bp)
app.register_blueprint(pattern_screen_api)

from app.main import main_bp
app.register_blueprint(main_bp)

Expand Down
47 changes: 47 additions & 0 deletions app/api/pattern_screen_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""形态选股 API 端点。"""
from flask import Blueprint, request, jsonify
from loguru import logger

pattern_screen_api = Blueprint('pattern_screen_api', __name__, url_prefix='/api/pattern-screen')


@pattern_screen_api.route('/groups', methods=['GET'])
def get_groups():
"""返回形态分组元数据(含命中数)。"""
try:
from app.services.pattern_screen_service import get_pattern_screen_service
svc = get_pattern_screen_service()
groups = svc.get_groups()
return jsonify({'code': 200, 'message': '成功', 'data': groups})
except Exception as e:
logger.error(f"获取形态分组失败: {e}")
return jsonify({'code': 500, 'message': f'服务器错误: {str(e)}', 'data': None}), 500


@pattern_screen_api.route('/screen', methods=['POST'])
def screen():
"""执行形态筛选,返回结果表格。"""
try:
data = request.get_json() or {}
patterns = data.get('patterns', [])
sort_by = data.get('sort_by', 'pct_chg')
order = data.get('order', 'desc')
limit = data.get('limit', 50)
offset = data.get('offset', 0)

from app.services.pattern_screen_service import get_pattern_screen_service
svc = get_pattern_screen_service()
result = svc.screen(
patterns=patterns,
sort_by=sort_by,
order=order,
limit=limit,
offset=offset,
)
return jsonify({'code': 200, 'message': '成功', 'data': result})
except ValueError as e:
logger.warning(f"形态筛选参数错误: {e}")
return jsonify({'code': 400, 'message': str(e), 'data': None}), 400
except Exception as e:
logger.error(f"形态筛选失败: {e}")
return jsonify({'code': 500, 'message': f'服务器错误: {str(e)}', 'data': None}), 500
30 changes: 30 additions & 0 deletions app/routes/heatmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""板块热力图页面路由。"""
from flask import Blueprint, render_template
from loguru import logger
from app.services.heatmap_service import HeatmapService

heatmap_routes = Blueprint('heatmap_routes', __name__, url_prefix='/heatmap')


@heatmap_routes.route('/')
def index():
"""板块热力图页面。"""
try:
service = HeatmapService()
sectors, stocks = service.get_heatmap_data()
trade_date = sectors[0]['trade_date'] if sectors else ''
return render_template(
'heatmap.html',
sectors_json=sectors,
stocks_json=stocks,
trade_date=trade_date,
)
except Exception as e:
logger.error(f"热力图加载失败: {e}")
return render_template(
'heatmap.html',
sectors_json=[],
stocks_json=[],
trade_date='',
error='数据加载失败,请确认 data/data.parquet 是否存在',
)
10 changes: 10 additions & 0 deletions app/routes/pattern_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""形态选股页面路由。"""
from flask import Blueprint, render_template

pattern_screen_bp = Blueprint('pattern_screen', __name__)


@pattern_screen_bp.route('/pattern-screen/')
def index():
"""形态选股页面。"""
return render_template('pattern_screen.html')
57 changes: 57 additions & 0 deletions app/services/heatmap_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""板块热力图数据服务 — 读取 data/data.parquet 并按行业聚合。"""
import os
import pandas as pd
import numpy as np
from loguru import logger


class HeatmapService:
"""板块热力图数据聚合服务。"""

def __init__(self):
self._data_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
'data', 'data.parquet'
)

def get_heatmap_data(self):
"""读取 parquet,返回 (sectors_json, stocks_json)。

Returns:
tuple: (list[dict], list[dict])
- sectors: 板块聚合数据,按 total_mv 降序
- stocks: 全量个股数据(已过滤停牌)
"""
df = pd.read_parquet(self._data_path)
trade_date = str(df['trade_date'].iloc[0])

# 过滤停牌/退市
df = df[df['close'] > 0].copy()

# 板块聚合
sectors = []
for industry, group in df.groupby('industry'):
total_mv_sum = group['total_mv'].sum()
if total_mv_sum == 0:
continue
avg_pct_chg = np.average(group['pct_chg'], weights=group['total_mv'])
sectors.append({
'name': industry,
'avg_pct_chg': round(avg_pct_chg, 2),
'total_mv': round(total_mv_sum, 2),
'stock_count': len(group),
'up_count': int((group['pct_chg'] > 0).sum()),
'down_count': int((group['pct_chg'] < 0).sum()),
'net_mf_amount': round(group['net_mf_amount'].sum(), 2),
'trade_date': trade_date,
})

sectors.sort(key=lambda x: x['total_mv'], reverse=True)

# 个股明细
stock_cols = ['ts_code', 'name', 'industry', 'pct_chg', 'close',
'total_mv', 'net_mf_amount', 'turnover_rate']
stocks_df = df[stock_cols].sort_values('pct_chg', ascending=False)
stocks = stocks_df.where(stocks_df.notna(), None).to_dict(orient='records')

return sectors, stocks
Loading
Loading