Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

45
app/__init__.py Normal file
View File

@@ -0,0 +1,45 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from datetime import datetime
import pytz
import os
# 创建Flask应用
app = Flask(__name__)
# 配置
config_name = os.environ.get('FLASK_ENV', 'development')
from config import config
app.config.from_object(config[config_name])
# 初始化扩展
db = SQLAlchemy(app)
CORS(app, resources={r"/api/*": {"origins": "*"}})
# 时区设置
def beijing_now():
"""获取北京时间"""
tz = pytz.timezone('Asia/Shanghai')
return datetime.now(tz)
# 导入模型
from app.models import *
# 创建数据库表
with app.app_context():
db.create_all()
# 注册路由
from app.routes import events, stocks, limitanalyse, calendar, industries
app.register_blueprint(events.bp)
app.register_blueprint(stocks.bp)
app.register_blueprint(limitanalyse.bp)
app.register_blueprint(calendar.bp)
app.register_blueprint(industries.bp)
if __name__ == '__main__':
print("=== Value Frontier React 架构启动 ===")
app.run(host='0.0.0.0', port=5001, debug=True)

Binary file not shown.

Binary file not shown.

Binary file not shown.

30
app/extensions.py Normal file
View File

@@ -0,0 +1,30 @@
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_compress import Compress
from flask_cors import CORS
from clickhouse_driver import Client as Cclient
from sqlalchemy import create_engine
# Database instances
db = SQLAlchemy()
# Other extensions
login_manager = LoginManager()
compress = Compress()
cors = CORS()
# Database engines (如果仍然需要直接使用 engine)
engine = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/stock", echo=False)
engine_med = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/med", echo=False)
engine_2 = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/valuefrontier", echo=False)
# ClickHouse client factory
def get_clickhouse_client():
return Cclient(
host='111.198.58.126',
port=18778,
user='default',
password='Zzl5588161!',
database='stock'
)

504
app/models.py Normal file
View File

@@ -0,0 +1,504 @@
from app import db
from datetime import datetime
import pytz
import json
def beijing_now():
"""获取北京时间"""
tz = pytz.timezone('Asia/Shanghai')
return datetime.now(tz)
class Post(db.Model):
"""帖子模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# 内容
title = db.Column(db.String(200)) # 标题(可选)
content = db.Column(db.Text, nullable=False) # 内容
content_type = db.Column(db.String(20), default='text') # 内容类型:text/rich_text/link
# 时间
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
# 统计
likes_count = db.Column(db.Integer, default=0)
comments_count = db.Column(db.Integer, default=0)
view_count = db.Column(db.Integer, default=0)
# 状态
status = db.Column(db.String(20), default='active') # active/hidden/deleted
is_top = db.Column(db.Boolean, default=False) # 是否置顶
# 关系
user = db.relationship('User', backref='posts')
likes = db.relationship('PostLike', backref='post', lazy='dynamic')
comments = db.relationship('Comment', backref='post', lazy='dynamic')
class User(db.Model):
"""用户模型"""
id = db.Column(db.Integer, primary_key=True)
# 基础账号信息(注册时必填)
username = db.Column(db.String(80), unique=True, nullable=False) # 用户名
email = db.Column(db.String(120), unique=True, nullable=False) # 邮箱
password_hash = db.Column(db.String(128), nullable=False) # 密码哈希
email_confirmed = db.Column(db.Boolean, default=False) # 邮箱是否验证
# 账号状态
created_at = db.Column(db.DateTime, default=beijing_now) # 注册时间
last_seen = db.Column(db.DateTime, default=beijing_now) # 最后活跃时间
status = db.Column(db.String(20), default='active') # 账号状态 active/banned/deleted
# 个人资料(可选,后续在个人中心完善)
nickname = db.Column(db.String(30)) # 社区昵称
avatar_url = db.Column(db.String(200)) # 头像URL
banner_url = db.Column(db.String(200)) # 个人主页背景图
bio = db.Column(db.String(200)) # 个人简介
gender = db.Column(db.String(10)) # 性别
birth_date = db.Column(db.Date) # 生日
location = db.Column(db.String(100)) # 所在地
# 联系方式(可选)
phone = db.Column(db.String(20)) # 手机号
wechat_id = db.Column(db.String(80)) # 微信号
# 实名认证信息(可选)
real_name = db.Column(db.String(30)) # 真实姓名
id_number = db.Column(db.String(18)) # 身份证号(加密存储)
is_verified = db.Column(db.Boolean, default=False) # 是否实名认证
verify_time = db.Column(db.DateTime) # 实名认证时间
# 投资相关信息(可选)
trading_experience = db.Column(db.Integer) # 炒股年限
investment_style = db.Column(db.String(50)) # 投资风格
risk_preference = db.Column(db.String(20)) # 风险偏好
investment_amount = db.Column(db.String(20)) # 投资规模
preferred_markets = db.Column(db.String(200), default='[]') # 偏好市场 JSON
# 社区信息(系统自动更新)
user_level = db.Column(db.Integer, default=1) # 用户等级
reputation_score = db.Column(db.Integer, default=0) # 信用积分
contribution_point = db.Column(db.Integer, default=0) # 贡献点数
post_count = db.Column(db.Integer, default=0) # 发帖数
comment_count = db.Column(db.Integer, default=0) # 评论数
follower_count = db.Column(db.Integer, default=0) # 粉丝数
following_count = db.Column(db.Integer, default=0) # 关注数
# 创作者信息(可选)
is_creator = db.Column(db.Boolean, default=False) # 是否创作者
creator_type = db.Column(db.String(20)) # 创作者类型
creator_tags = db.Column(db.String(200), default='[]') # 创作者标签 JSON
# 系统设置
email_notifications = db.Column(db.Boolean, default=True) # 邮件通知
sms_notifications = db.Column(db.Boolean, default=False) # 短信通知
wechat_notifications = db.Column(db.Boolean, default=False) # 微信通知
notification_preferences = db.Column(db.String(500), default='{}') # 通知偏好 JSON
privacy_level = db.Column(db.String(20), default='public') # 隐私级别
theme_preference = db.Column(db.String(20), default='light') # 主题偏好
blocked_keywords = db.Column(db.String(500), default='[]') # 屏蔽关键词 JSON
# 手机号验证
phone_confirmed = db.Column(db.Boolean, default=False) # 手机是否验证
phone_confirm_time = db.Column(db.DateTime) # 手机验证时间
def __init__(self, username, email=None, password=None, phone=None):
self.username = username
if email:
self.email = email
if password:
self.set_password(password)
if phone:
self.phone = phone
def set_password(self, password):
from werkzeug.security import generate_password_hash
self.password_hash = generate_password_hash(password)
def check_password(self, password):
from werkzeug.security import check_password_hash
return check_password_hash(self.password_hash, password)
def update_last_seen(self):
self.last_seen = beijing_now()
db.session.commit()
def get_preferred_markets(self):
try:
return json.loads(self.preferred_markets)
except (json.JSONDecodeError, TypeError):
return []
def get_blocked_keywords(self):
try:
return json.loads(self.blocked_keywords)
except (json.JSONDecodeError, TypeError):
return []
def get_notification_preferences(self):
try:
return json.loads(self.notification_preferences)
except (json.JSONDecodeError, TypeError):
return {}
def get_creator_tags(self):
try:
return json.loads(self.creator_tags)
except (json.JSONDecodeError, TypeError):
return []
def set_preferred_markets(self, markets):
self.preferred_markets = json.dumps(markets)
def set_blocked_keywords(self, keywords):
self.blocked_keywords = json.dumps(keywords)
def set_notification_preferences(self, preferences):
self.notification_preferences = json.dumps(preferences)
def set_creator_tags(self, tags):
self.creator_tags = json.dumps(tags)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'nickname': self.nickname,
'avatar_url': self.avatar_url,
'bio': self.bio,
'created_at': self.created_at.isoformat() if self.created_at else None,
'last_seen': self.last_seen.isoformat() if self.last_seen else None,
'status': self.status,
'user_level': self.user_level,
'reputation_score': self.reputation_score,
'post_count': self.post_count,
'follower_count': self.follower_count,
'following_count': self.following_count
}
def __repr__(self):
return f'<User {self.username}>'
class Comment(db.Model):
"""评论"""
id = db.Column(db.Integer, primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
content = db.Column(db.Text, nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('comment.id')) # 父评论ID,用于回复
created_at = db.Column(db.DateTime, default=beijing_now)
status = db.Column(db.String(20), default='active')
user = db.relationship('User', backref='comments')
replies = db.relationship('Comment', backref=db.backref('parent', remote_side=[id]))
class CommentLike(db.Model):
"""评论点赞记录基于session_id以兼容匿名点赞"""
__tablename__ = 'comment_like'
id = db.Column(db.Integer, primary_key=True)
comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False)
session_id = db.Column(db.String(100), nullable=False)
created_at = db.Column(db.DateTime, default=beijing_now)
__table_args__ = (db.UniqueConstraint('comment_id', 'session_id'),)
class EventFollow(db.Model):
"""事件关注"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
created_at = db.Column(db.DateTime, default=beijing_now)
user = db.relationship('User', backref='event_follows')
__table_args__ = (db.UniqueConstraint('user_id', 'event_id'),)
class PostLike(db.Model):
"""帖子点赞"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
created_at = db.Column(db.DateTime, default=beijing_now)
user = db.relationship('User', backref='post_likes')
__table_args__ = (db.UniqueConstraint('user_id', 'post_id'),)
class Event(db.Model):
"""事件模型"""
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
# 事件类型与状态
event_type = db.Column(db.String(50))
status = db.Column(db.String(20), default='active')
# 时间相关
start_time = db.Column(db.DateTime, default=beijing_now)
end_time = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now)
# 热度与统计
hot_score = db.Column(db.Float, default=0)
view_count = db.Column(db.Integer, default=0)
trending_score = db.Column(db.Float, default=0)
post_count = db.Column(db.Integer, default=0)
follower_count = db.Column(db.Integer, default=0)
# 关联信息
related_industries = db.Column(db.JSON)
keywords = db.Column(db.JSON)
files = db.Column(db.JSON)
importance = db.Column(db.String(20))
related_avg_chg = db.Column(db.Float, default=0)
related_max_chg = db.Column(db.Float, default=0)
related_week_chg = db.Column(db.Float, default=0)
# 新增字段
invest_score = db.Column(db.Integer) # 超预期得分
expectation_surprise_score = db.Column(db.Integer)
# 创建者信息
creator_id = db.Column(db.Integer, db.ForeignKey('user.id'))
creator = db.relationship('User', backref='created_events')
# 关系
posts = db.relationship('Post', backref='event', lazy='dynamic')
followers = db.relationship('EventFollow', backref='event', lazy='dynamic')
related_stocks = db.relationship('RelatedStock', backref='event', lazy='dynamic')
historical_events = db.relationship('HistoricalEvent', backref='event', lazy='dynamic')
related_data = db.relationship('RelatedData', backref='event', lazy='dynamic')
related_concepts = db.relationship('RelatedConcepts', backref='event', lazy='dynamic')
@property
def keywords_list(self):
if isinstance(self.keywords, list):
return self.keywords
elif isinstance(self.keywords, str):
try:
return json.loads(self.keywords)
except (json.JSONDecodeError, TypeError):
return []
return []
def set_keywords(self, keywords):
if isinstance(keywords, list):
self.keywords = keywords
elif isinstance(keywords, str):
try:
self.keywords = json.loads(keywords)
except json.JSONDecodeError:
self.keywords = [keywords]
else:
self.keywords = []
class RelatedStock(db.Model):
"""相关标的模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
stock_code = db.Column(db.String(20)) # 股票代码
stock_name = db.Column(db.String(100)) # 股票名称
sector = db.Column(db.String(100)) # 关联类型
relation_desc = db.Column(db.String(1024)) # 关联原因描述
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
correlation = db.Column(db.Float())
momentum = db.Column(db.String(1024)) #动量
class RelatedData(db.Model):
"""关联数据模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
title = db.Column(db.String(200)) # 数据标题
data_type = db.Column(db.String(50)) # 数据类型
data_content = db.Column(db.JSON) # 数据内容(JSON格式)
description = db.Column(db.Text) # 数据描述
created_at = db.Column(db.DateTime, default=beijing_now)
class RelatedConcepts(db.Model):
"""关联数据模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
concept_code = db.Column(db.String(20)) # 数据标题
concept = db.Column(db.String(100)) # 数据类型
reason = db.Column(db.Text) # 数据描述
image_paths = db.Column(db.JSON) # 数据内容(JSON格式)
created_at = db.Column(db.DateTime, default=beijing_now)
@property
def image_paths_list(self):
if isinstance(self.image_paths, list):
return self.image_paths
elif isinstance(self.image_paths, str):
try:
return json.loads(self.image_paths)
except (json.JSONDecodeError, TypeError):
return []
return []
def set_image_paths(self, image_paths):
if isinstance(image_paths, list):
self.image_paths = image_paths
elif isinstance(image_paths, str):
try:
self.image_paths = json.loads(image_paths)
except json.JSONDecodeError:
self.image_paths = [image_paths]
else:
self.image_paths = []
def get_first_image_path(self):
paths = self.image_paths_list
return paths[0] if paths else None
class EventHotHistory(db.Model):
"""事件热度历史记录"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
score = db.Column(db.Float) # 总分
interaction_score = db.Column(db.Float) # 互动分数
follow_score = db.Column(db.Float) # 关注度分数
view_score = db.Column(db.Float) # 浏览量分数
recent_activity_score = db.Column(db.Float) # 最近活跃度分数
time_decay = db.Column(db.Float) # 时间衰减因子
created_at = db.Column(db.DateTime, default=beijing_now)
event = db.relationship('Event', backref='hot_history')
class EventTransmissionNode(db.Model):
"""事件传导节点模型"""
__tablename__ = 'event_transmission_nodes'
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
node_type = db.Column(db.Enum('company', 'industry', 'policy', 'technology',
'market', 'event', 'other'), nullable=False)
node_name = db.Column(db.String(200), nullable=False)
node_description = db.Column(db.Text)
importance_score = db.Column(db.Integer, default=50)
stock_code = db.Column(db.String(20))
is_main_event = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
# Relationships
event = db.relationship('Event', backref='transmission_nodes')
outgoing_edges = db.relationship('EventTransmissionEdge',
foreign_keys='EventTransmissionEdge.from_node_id',
backref='from_node', cascade='all, delete-orphan')
incoming_edges = db.relationship('EventTransmissionEdge',
foreign_keys='EventTransmissionEdge.to_node_id',
backref='to_node', cascade='all, delete-orphan')
__table_args__ = (
db.Index('idx_event_node_type', 'event_id', 'node_type'),
db.Index('idx_node_name', 'node_name'),
)
class EventTransmissionEdge(db.Model):
"""事件传导边模型"""
__tablename__ = 'event_transmission_edges'
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
from_node_id = db.Column(db.Integer, db.ForeignKey('event_transmission_nodes.id'), nullable=False)
to_node_id = db.Column(db.Integer, db.ForeignKey('event_transmission_nodes.id'), nullable=False)
transmission_type = db.Column(db.Enum('supply_chain', 'competition', 'policy',
'technology', 'capital_flow', 'expectation',
'cyclic_effect', 'other'), nullable=False)
transmission_mechanism = db.Column(db.Text)
direction = db.Column(db.Enum('positive', 'negative', 'neutral', 'mixed'), default='neutral')
strength = db.Column(db.Integer, default=50)
impact = db.Column(db.Text)
is_circular = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
# Relationship
event = db.relationship('Event', backref='transmission_edges')
__table_args__ = (
db.Index('idx_event_edge_type', 'event_id', 'transmission_type'),
db.Index('idx_from_to_nodes', 'from_node_id', 'to_node_id'),
)
class EventSankeyFlow(db.Model):
"""事件桑基流模型"""
__tablename__ = 'event_sankey_flows'
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
# 流的基本信息
source_node = db.Column(db.String(200), nullable=False)
source_type = db.Column(db.Enum('event', 'policy', 'technology', 'industry',
'company', 'product'), nullable=False)
source_level = db.Column(db.Integer, nullable=False, default=0)
target_node = db.Column(db.String(200), nullable=False)
target_type = db.Column(db.Enum('policy', 'technology', 'industry',
'company', 'product'), nullable=False)
target_level = db.Column(db.Integer, nullable=False, default=1)
# 流量信息
flow_value = db.Column(db.Numeric(10, 2), nullable=False)
flow_ratio = db.Column(db.Numeric(5, 4), nullable=False)
# 传导机制
transmission_path = db.Column(db.String(500))
impact_description = db.Column(db.Text)
evidence_strength = db.Column(db.Integer, default=50)
# 时间戳
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
# 关系
event = db.relationship('Event', backref='sankey_flows')
__table_args__ = (
db.Index('idx_event_flow', 'event_id'),
db.Index('idx_source_target', 'source_node', 'target_node'),
)
class HistoricalEvent(db.Model):
"""历史事件模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
title = db.Column(db.String(200))
content = db.Column(db.Text)
event_date = db.Column(db.DateTime)
relevance = db.Column(db.Integer) # 相关性
importance = db.Column(db.Integer) # 重要程度
related_stock = db.Column(db.JSON) # 保留JSON字段
created_at = db.Column(db.DateTime, default=beijing_now)
# 新增关系
stocks = db.relationship('HistoricalEventStock', backref='historical_event', lazy='dynamic',
cascade='all, delete-orphan')
class HistoricalEventStock(db.Model):
"""历史事件相关股票模型"""
__tablename__ = 'historical_event_stocks'
id = db.Column(db.Integer, primary_key=True)
historical_event_id = db.Column(db.Integer, db.ForeignKey('historical_event.id'), nullable=False)
stock_code = db.Column(db.String(20), nullable=False)
stock_name = db.Column(db.String(50))
relation_desc = db.Column(db.Text)
correlation = db.Column(db.Float, default=0.5)
sector = db.Column(db.String(100))
created_at = db.Column(db.DateTime, default=beijing_now)
__table_args__ = (
db.Index('idx_historical_event_stock', 'historical_event_id', 'stock_code'),
)

1
app/routes/__init__.py Normal file
View File

@@ -0,0 +1 @@
# 路由包初始化文件

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

121
app/routes/calendar.py Normal file
View File

@@ -0,0 +1,121 @@
from flask import Blueprint, request, jsonify
from datetime import datetime, timedelta
import json
bp = Blueprint('calendar', __name__, url_prefix='/api/v1/calendar')
@bp.route('/event-counts', methods=['GET'])
def get_event_counts():
"""获取事件数量统计"""
try:
year = request.args.get('year', '2027')
month = request.args.get('month', '10')
# 模拟事件数量数据
event_counts = []
for day in range(1, 32):
count = (day % 7) + 1 # 模拟每天1-7个事件
event_counts.append({
'date': f'{year}-{month.zfill(2)}-{day:02d}',
'count': count
})
return jsonify({
'success': True,
'data': event_counts
})
except Exception as e:
print(f"Error getting event counts: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/events', methods=['GET'])
def get_calendar_events():
"""获取日历事件"""
try:
year = request.args.get('year', '2027')
month = request.args.get('month', '10')
event_type = request.args.get('type', 'all')
# 模拟日历事件数据
events = []
for day in range(1, 32):
for i in range((day % 7) + 1):
event = {
'id': f'{year}{month.zfill(2)}{day:02d}{i}',
'title': f'事件{day}-{i+1}',
'date': f'{year}-{month.zfill(2)}-{day:02d}',
'type': ['政策', '技术', '产业', '公司'][i % 4],
'importance': ['', '', ''][i % 3],
'status': 'active'
}
events.append(event)
# 根据类型过滤
if event_type != 'all':
events = [e for e in events if e['type'] == event_type]
return jsonify({
'success': True,
'data': events
})
except Exception as e:
print(f"Error getting calendar events: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/events/<int:event_id>', methods=['GET'])
def get_calendar_event_detail(event_id):
"""获取日历事件详情"""
try:
# 模拟事件详情
event_detail = {
'id': event_id,
'title': f'事件{event_id}详情',
'description': f'这是事件{event_id}的详细描述',
'date': '2027-10-15',
'type': '政策',
'importance': '',
'status': 'active',
'related_stocks': [
{'code': '000001', 'name': '股票A'},
{'code': '000002', 'name': '股票B'}
],
'keywords': ['政策', '改革', '创新'],
'files': [
{'name': '报告.pdf', 'url': '/files/report.pdf'},
{'name': '数据.xlsx', 'url': '/files/data.xlsx'}
]
}
return jsonify({
'success': True,
'data': event_detail
})
except Exception as e:
print(f"Error getting calendar event detail: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def get_event_class(count):
"""根据事件数量获取CSS类"""
if count == 0:
return 'no-events'
elif count <= 3:
return 'few-events'
elif count <= 6:
return 'medium-events'
else:
return 'many-events'
def parse_json_field(field_value):
"""解析JSON字段"""
if isinstance(field_value, str):
try:
return json.loads(field_value)
except (json.JSONDecodeError, TypeError):
return []
elif isinstance(field_value, (list, dict)):
return field_value
else:
return []

385
app/routes/events.py Normal file
View File

@@ -0,0 +1,385 @@
from flask import Blueprint, request, jsonify
from app import db
from app.models import Event, RelatedStock, RelatedConcepts, HistoricalEvent, EventTransmissionNode, EventTransmissionEdge, EventSankeyFlow
from datetime import datetime
import json
bp = Blueprint('events', __name__, url_prefix='/api/events')
@bp.route('/<int:event_id>', methods=['GET'])
def get_event_detail(event_id):
"""获取事件详情"""
try:
event = Event.query.get(event_id)
if not event:
return jsonify({'success': False, 'error': '事件不存在'}), 404
# 获取相关股票
related_stocks = RelatedStock.query.filter_by(event_id=event_id).all()
stocks_data = []
for stock in related_stocks:
stocks_data.append({
'id': stock.id,
'stock_code': stock.stock_code,
'stock_name': stock.stock_name,
'sector': stock.sector,
'relation_desc': stock.relation_desc,
'correlation': stock.correlation,
'momentum': stock.momentum,
'created_at': stock.created_at.isoformat() if stock.created_at else None
})
# 获取相关概念
related_concepts = RelatedConcepts.query.filter_by(event_id=event_id).all()
concepts_data = []
for concept in related_concepts:
concepts_data.append({
'id': concept.id,
'concept_code': concept.concept_code,
'concept': concept.concept,
'reason': concept.reason,
'image_paths': concept.image_paths_list,
'created_at': concept.created_at.isoformat() if concept.created_at else None
})
event_data = {
'id': event.id,
'title': event.title,
'description': event.description,
'event_type': event.event_type,
'status': event.status,
'start_time': event.start_time.isoformat() if event.start_time else None,
'end_time': event.end_time.isoformat() if event.end_time else None,
'created_at': event.created_at.isoformat() if event.created_at else None,
'updated_at': event.updated_at.isoformat() if event.updated_at else None,
'hot_score': event.hot_score,
'view_count': event.view_count,
'trending_score': event.trending_score,
'post_count': event.post_count,
'follower_count': event.follower_count,
'related_industries': event.related_industries,
'keywords': event.keywords_list,
'files': event.files,
'importance': event.importance,
'related_avg_chg': event.related_avg_chg,
'related_max_chg': event.related_max_chg,
'related_week_chg': event.related_week_chg,
'invest_score': event.invest_score,
'expectation_surprise_score': event.expectation_surprise_score,
'related_stocks': stocks_data,
'related_concepts': concepts_data
}
return jsonify({
'success': True,
'data': event_data
})
except Exception as e:
print(f"Error getting event detail: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/stocks', methods=['GET'])
def get_related_stocks(event_id):
"""获取事件相关股票"""
try:
stocks = RelatedStock.query.filter_by(event_id=event_id).all()
stocks_data = []
for stock in stocks:
stocks_data.append({
'id': stock.id,
'stock_code': stock.stock_code,
'stock_name': stock.stock_name,
'sector': stock.sector,
'relation_desc': stock.relation_desc,
'correlation': stock.correlation,
'momentum': stock.momentum,
'created_at': stock.created_at.isoformat() if stock.created_at else None
})
return jsonify({
'success': True,
'data': stocks_data
})
except Exception as e:
print(f"Error getting related stocks: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/stocks', methods=['POST'])
def add_related_stock(event_id):
"""添加相关股票"""
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '请提供数据'}), 400
# 检查事件是否存在
event = Event.query.get(event_id)
if not event:
return jsonify({'success': False, 'error': '事件不存在'}), 404
# 创建新的相关股票记录
new_stock = RelatedStock(
event_id=event_id,
stock_code=data['stock_code'],
stock_name=data.get('stock_name', ''),
sector=data.get('sector', ''),
relation_desc=data['relation_desc'],
correlation=data.get('correlation', 0.5),
momentum=data.get('momentum', '')
)
db.session.add(new_stock)
db.session.commit()
return jsonify({
'success': True,
'message': '相关股票添加成功',
'data': {
'id': new_stock.id,
'stock_code': new_stock.stock_code,
'stock_name': new_stock.stock_name,
'sector': new_stock.sector,
'relation_desc': new_stock.relation_desc,
'correlation': new_stock.correlation,
'momentum': new_stock.momentum
}
})
except Exception as e:
db.session.rollback()
print(f"Error adding related stock: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/stocks/<int:stock_id>', methods=['DELETE'])
def delete_related_stock(stock_id):
"""删除相关股票"""
try:
stock = RelatedStock.query.get(stock_id)
if not stock:
return jsonify({'success': False, 'error': '相关股票不存在'}), 404
db.session.delete(stock)
db.session.commit()
return jsonify({
'success': True,
'message': '相关股票删除成功'
})
except Exception as e:
db.session.rollback()
print(f"Error deleting related stock: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/concepts', methods=['GET'])
def get_related_concepts(event_id):
"""获取事件相关概念"""
try:
concepts = RelatedConcepts.query.filter_by(event_id=event_id).all()
concepts_data = []
for concept in concepts:
concepts_data.append({
'id': concept.id,
'concept_code': concept.concept_code,
'concept': concept.concept,
'reason': concept.reason,
'image_paths': concept.image_paths_list,
'created_at': concept.created_at.isoformat() if concept.created_at else None
})
return jsonify({
'success': True,
'data': concepts_data
})
except Exception as e:
print(f"Error getting related concepts: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/historical', methods=['GET'])
def get_historical_events(event_id):
"""获取历史事件"""
try:
historical_events = HistoricalEvent.query.filter_by(event_id=event_id).all()
events_data = []
for event in historical_events:
events_data.append({
'id': event.id,
'title': event.title,
'content': event.content,
'event_date': event.event_date.isoformat() if event.event_date else None,
'relevance': event.relevance,
'importance': event.importance,
'related_stock': event.related_stock,
'created_at': event.created_at.isoformat() if event.created_at else None
})
return jsonify({
'success': True,
'data': events_data
})
except Exception as e:
print(f"Error getting historical events: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/expectation-score', methods=['GET'])
def get_expectation_score(event_id):
"""获取超预期得分"""
try:
event = Event.query.get(event_id)
if not event:
return jsonify({'success': False, 'error': '事件不存在'}), 404
return jsonify({
'success': True,
'data': {
'invest_score': event.invest_score,
'expectation_surprise_score': event.expectation_surprise_score
}
})
except Exception as e:
print(f"Error getting expectation score: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/follow', methods=['POST'])
def toggle_event_follow(event_id):
"""关注/取消关注事件"""
try:
# 这里需要用户认证,暂时返回成功
return jsonify({
'success': True,
'message': '关注状态更新成功'
})
except Exception as e:
print(f"Error toggling event follow: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/transmission', methods=['GET'])
def get_transmission_chain(event_id):
"""获取事件传导链"""
try:
# 获取传导节点
nodes = EventTransmissionNode.query.filter_by(event_id=event_id).all()
nodes_data = []
for node in nodes:
nodes_data.append({
'id': node.id,
'node_type': node.node_type,
'node_name': node.node_name,
'node_description': node.node_description,
'importance_score': node.importance_score,
'stock_code': node.stock_code,
'is_main_event': node.is_main_event
})
# 获取传导边
edges = EventTransmissionEdge.query.filter_by(event_id=event_id).all()
edges_data = []
for edge in edges:
edges_data.append({
'id': edge.id,
'from_node_id': edge.from_node_id,
'to_node_id': edge.to_node_id,
'transmission_type': edge.transmission_type,
'transmission_mechanism': edge.transmission_mechanism,
'direction': edge.direction,
'strength': edge.strength,
'impact': edge.impact,
'is_circular': edge.is_circular
})
return jsonify({
'success': True,
'data': {
'nodes': nodes_data,
'edges': edges_data
}
})
except Exception as e:
print(f"Error getting transmission chain: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/sankey-data')
def get_event_sankey_data(event_id):
"""获取事件桑基图数据"""
try:
flows = EventSankeyFlow.query.filter_by(event_id=event_id).all()
flows_data = []
for flow in flows:
flows_data.append({
'id': flow.id,
'source_node': flow.source_node,
'source_type': flow.source_type,
'source_level': flow.source_level,
'target_node': flow.target_node,
'target_type': flow.target_type,
'target_level': flow.target_level,
'flow_value': float(flow.flow_value),
'flow_ratio': float(flow.flow_ratio),
'transmission_path': flow.transmission_path,
'impact_description': flow.impact_description,
'evidence_strength': flow.evidence_strength
})
return jsonify({
'success': True,
'data': flows_data
})
except Exception as e:
print(f"Error getting sankey data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/chain-analysis')
def get_event_chain_analysis(event_id):
"""获取事件链分析"""
try:
# 这里可以添加更复杂的链分析逻辑
return jsonify({
'success': True,
'data': {
'event_id': event_id,
'analysis': '链分析数据'
}
})
except Exception as e:
print(f"Error getting chain analysis: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/chain-node/<int:node_id>', methods=['GET'])
def get_chain_node_detail(event_id, node_id):
"""获取链节点详情"""
try:
node = EventTransmissionNode.query.filter_by(
event_id=event_id,
id=node_id
).first()
if not node:
return jsonify({'success': False, 'error': '节点不存在'}), 404
return jsonify({
'success': True,
'data': {
'id': node.id,
'node_type': node.node_type,
'node_name': node.node_name,
'node_description': node.node_description,
'importance_score': node.importance_score,
'stock_code': node.stock_code,
'is_main_event': node.is_main_event
}
})
except Exception as e:
print(f"Error getting chain node detail: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

511
app/routes/industries.py Normal file
View File

@@ -0,0 +1,511 @@
from flask import Blueprint, request, jsonify
import json
bp = Blueprint('industries', __name__, url_prefix='/api')
@bp.route('/classifications', methods=['GET'])
def get_classifications():
"""获取行业分类"""
try:
# 模拟行业分类数据
classifications = [
{
'id': 1,
'name': '申万一级行业',
'description': '申万一级行业分类标准',
'levels': [
{'id': 1, 'name': '农林牧渔'},
{'id': 2, 'name': '采掘'},
{'id': 3, 'name': '化工'},
{'id': 4, 'name': '钢铁'},
{'id': 5, 'name': '有色金属'},
{'id': 6, 'name': '建筑材料'},
{'id': 7, 'name': '建筑装饰'},
{'id': 8, 'name': '电气设备'},
{'id': 9, 'name': '国防军工'},
{'id': 10, 'name': '汽车'},
{'id': 11, 'name': '家用电器'},
{'id': 12, 'name': '纺织服装'},
{'id': 13, 'name': '轻工制造'},
{'id': 14, 'name': '医药生物'},
{'id': 15, 'name': '公用事业'},
{'id': 16, 'name': '交通运输'},
{'id': 17, 'name': '房地产'},
{'id': 18, 'name': '商业贸易'},
{'id': 19, 'name': '休闲服务'},
{'id': 20, 'name': '银行'},
{'id': 21, 'name': '非银金融'},
{'id': 22, 'name': '综合'},
{'id': 23, 'name': '计算机'},
{'id': 24, 'name': '传媒'},
{'id': 25, 'name': '通信'},
{'id': 26, 'name': '电子'},
{'id': 27, 'name': '机械设备'},
{'id': 28, 'name': '食品饮料'}
]
}
]
return jsonify({
'success': True,
'data': classifications
})
except Exception as e:
print(f"Error getting classifications: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/levels', methods=['GET'])
def get_industry_levels():
"""获取行业层级"""
try:
classification_id = request.args.get('classification_id', '1')
# 模拟行业层级数据
levels = [
{
'id': 1,
'name': '农林牧渔',
'code': '801010',
'description': '农业、林业、畜牧业、渔业',
'stock_count': 45,
'avg_change': 1.2,
'total_market_cap': 500000000000,
'sub_industries': [
{'id': 101, 'name': '种植业', 'stock_count': 20},
{'id': 102, 'name': '林业', 'stock_count': 8},
{'id': 103, 'name': '畜牧业', 'stock_count': 12},
{'id': 104, 'name': '渔业', 'stock_count': 5}
]
},
{
'id': 2,
'name': '采掘',
'code': '801020',
'description': '煤炭、石油、天然气、有色金属矿采选',
'stock_count': 38,
'avg_change': 0.8,
'total_market_cap': 800000000000,
'sub_industries': [
{'id': 201, 'name': '煤炭开采', 'stock_count': 15},
{'id': 202, 'name': '石油开采', 'stock_count': 8},
{'id': 203, 'name': '有色金属矿采选', 'stock_count': 15}
]
},
{
'id': 3,
'name': '化工',
'code': '801030',
'description': '化学原料、化学制品、化学纤维',
'stock_count': 156,
'avg_change': 1.5,
'total_market_cap': 1200000000000,
'sub_industries': [
{'id': 301, 'name': '化学原料', 'stock_count': 45},
{'id': 302, 'name': '化学制品', 'stock_count': 78},
{'id': 303, 'name': '化学纤维', 'stock_count': 33}
]
},
{
'id': 4,
'name': '钢铁',
'code': '801040',
'description': '钢铁冶炼、钢铁制品',
'stock_count': 32,
'avg_change': 0.6,
'total_market_cap': 600000000000,
'sub_industries': [
{'id': 401, 'name': '钢铁冶炼', 'stock_count': 18},
{'id': 402, 'name': '钢铁制品', 'stock_count': 14}
]
},
{
'id': 5,
'name': '有色金属',
'code': '801050',
'description': '有色金属冶炼、有色金属制品',
'stock_count': 67,
'avg_change': 1.8,
'total_market_cap': 900000000000,
'sub_industries': [
{'id': 501, 'name': '有色金属冶炼', 'stock_count': 35},
{'id': 502, 'name': '有色金属制品', 'stock_count': 32}
]
},
{
'id': 6,
'name': '建筑材料',
'code': '801060',
'description': '水泥、玻璃、陶瓷、其他建材',
'stock_count': 89,
'avg_change': 1.1,
'total_market_cap': 700000000000,
'sub_industries': [
{'id': 601, 'name': '水泥', 'stock_count': 25},
{'id': 602, 'name': '玻璃', 'stock_count': 18},
{'id': 603, 'name': '陶瓷', 'stock_count': 12},
{'id': 604, 'name': '其他建材', 'stock_count': 34}
]
},
{
'id': 7,
'name': '建筑装饰',
'code': '801070',
'description': '房屋建设、装修装饰、园林工程',
'stock_count': 45,
'avg_change': 0.9,
'total_market_cap': 400000000000,
'sub_industries': [
{'id': 701, 'name': '房屋建设', 'stock_count': 15},
{'id': 702, 'name': '装修装饰', 'stock_count': 20},
{'id': 703, 'name': '园林工程', 'stock_count': 10}
]
},
{
'id': 8,
'name': '电气设备',
'code': '801080',
'description': '电机、电气自动化设备、电源设备',
'stock_count': 134,
'avg_change': 2.1,
'total_market_cap': 1500000000000,
'sub_industries': [
{'id': 801, 'name': '电机', 'stock_count': 25},
{'id': 802, 'name': '电气自动化设备', 'stock_count': 45},
{'id': 803, 'name': '电源设备', 'stock_count': 64}
]
},
{
'id': 9,
'name': '国防军工',
'code': '801090',
'description': '航天装备、航空装备、地面兵装',
'stock_count': 28,
'avg_change': 1.6,
'total_market_cap': 300000000000,
'sub_industries': [
{'id': 901, 'name': '航天装备', 'stock_count': 8},
{'id': 902, 'name': '航空装备', 'stock_count': 12},
{'id': 903, 'name': '地面兵装', 'stock_count': 8}
]
},
{
'id': 10,
'name': '汽车',
'code': '801100',
'description': '汽车整车、汽车零部件',
'stock_count': 78,
'avg_change': 1.3,
'total_market_cap': 1100000000000,
'sub_industries': [
{'id': 1001, 'name': '汽车整车', 'stock_count': 25},
{'id': 1002, 'name': '汽车零部件', 'stock_count': 53}
]
},
{
'id': 11,
'name': '家用电器',
'code': '801110',
'description': '白色家电、小家电、家电零部件',
'stock_count': 56,
'avg_change': 1.0,
'total_market_cap': 800000000000,
'sub_industries': [
{'id': 1101, 'name': '白色家电', 'stock_count': 20},
{'id': 1102, 'name': '小家电', 'stock_count': 18},
{'id': 1103, 'name': '家电零部件', 'stock_count': 18}
]
},
{
'id': 12,
'name': '纺织服装',
'code': '801120',
'description': '纺织制造、服装家纺',
'stock_count': 67,
'avg_change': 0.7,
'total_market_cap': 500000000000,
'sub_industries': [
{'id': 1201, 'name': '纺织制造', 'stock_count': 35},
{'id': 1202, 'name': '服装家纺', 'stock_count': 32}
]
},
{
'id': 13,
'name': '轻工制造',
'code': '801130',
'description': '造纸、包装印刷、家用轻工',
'stock_count': 89,
'avg_change': 0.9,
'total_market_cap': 600000000000,
'sub_industries': [
{'id': 1301, 'name': '造纸', 'stock_count': 25},
{'id': 1302, 'name': '包装印刷', 'stock_count': 30},
{'id': 1303, 'name': '家用轻工', 'stock_count': 34}
]
},
{
'id': 14,
'name': '医药生物',
'code': '801140',
'description': '化学制药、中药、生物制品、医疗器械',
'stock_count': 234,
'avg_change': 1.9,
'total_market_cap': 2500000000000,
'sub_industries': [
{'id': 1401, 'name': '化学制药', 'stock_count': 78},
{'id': 1402, 'name': '中药', 'stock_count': 45},
{'id': 1403, 'name': '生物制品', 'stock_count': 56},
{'id': 1404, 'name': '医疗器械', 'stock_count': 55}
]
},
{
'id': 15,
'name': '公用事业',
'code': '801150',
'description': '电力、燃气、水务',
'stock_count': 78,
'avg_change': 0.5,
'total_market_cap': 900000000000,
'sub_industries': [
{'id': 1501, 'name': '电力', 'stock_count': 45},
{'id': 1502, 'name': '燃气', 'stock_count': 18},
{'id': 1503, 'name': '水务', 'stock_count': 15}
]
},
{
'id': 16,
'name': '交通运输',
'code': '801160',
'description': '港口、公路、铁路、航空',
'stock_count': 67,
'avg_change': 0.8,
'total_market_cap': 800000000000,
'sub_industries': [
{'id': 1601, 'name': '港口', 'stock_count': 15},
{'id': 1602, 'name': '公路', 'stock_count': 20},
{'id': 1603, 'name': '铁路', 'stock_count': 12},
{'id': 1604, 'name': '航空', 'stock_count': 20}
]
},
{
'id': 17,
'name': '房地产',
'code': '801170',
'description': '房地产开发、房地产服务',
'stock_count': 89,
'avg_change': 0.6,
'total_market_cap': 1200000000000,
'sub_industries': [
{'id': 1701, 'name': '房地产开发', 'stock_count': 65},
{'id': 1702, 'name': '房地产服务', 'stock_count': 24}
]
},
{
'id': 18,
'name': '商业贸易',
'code': '801180',
'description': '贸易、零售',
'stock_count': 78,
'avg_change': 0.7,
'total_market_cap': 600000000000,
'sub_industries': [
{'id': 1801, 'name': '贸易', 'stock_count': 35},
{'id': 1802, 'name': '零售', 'stock_count': 43}
]
},
{
'id': 19,
'name': '休闲服务',
'code': '801190',
'description': '景点、酒店、旅游综合',
'stock_count': 34,
'avg_change': 1.2,
'total_market_cap': 300000000000,
'sub_industries': [
{'id': 1901, 'name': '景点', 'stock_count': 12},
{'id': 1902, 'name': '酒店', 'stock_count': 15},
{'id': 1903, 'name': '旅游综合', 'stock_count': 7}
]
},
{
'id': 20,
'name': '银行',
'code': '801200',
'description': '银行',
'stock_count': 28,
'avg_change': 0.4,
'total_market_cap': 8000000000000,
'sub_industries': [
{'id': 2001, 'name': '银行', 'stock_count': 28}
]
},
{
'id': 21,
'name': '非银金融',
'code': '801210',
'description': '保险、证券、多元金融',
'stock_count': 45,
'avg_change': 0.8,
'total_market_cap': 2000000000000,
'sub_industries': [
{'id': 2101, 'name': '保险', 'stock_count': 8},
{'id': 2102, 'name': '证券', 'stock_count': 25},
{'id': 2103, 'name': '多元金融', 'stock_count': 12}
]
},
{
'id': 22,
'name': '综合',
'code': '801220',
'description': '综合',
'stock_count': 23,
'avg_change': 0.6,
'total_market_cap': 200000000000,
'sub_industries': [
{'id': 2201, 'name': '综合', 'stock_count': 23}
]
},
{
'id': 23,
'name': '计算机',
'code': '801230',
'description': '计算机设备、计算机应用',
'stock_count': 156,
'avg_change': 2.3,
'total_market_cap': 1800000000000,
'sub_industries': [
{'id': 2301, 'name': '计算机设备', 'stock_count': 45},
{'id': 2302, 'name': '计算机应用', 'stock_count': 111}
]
},
{
'id': 24,
'name': '传媒',
'code': '801240',
'description': '文化传媒、营销传播',
'stock_count': 78,
'avg_change': 1.4,
'total_market_cap': 700000000000,
'sub_industries': [
{'id': 2401, 'name': '文化传媒', 'stock_count': 45},
{'id': 2402, 'name': '营销传播', 'stock_count': 33}
]
},
{
'id': 25,
'name': '通信',
'code': '801250',
'description': '通信设备、通信运营',
'stock_count': 45,
'avg_change': 1.7,
'total_market_cap': 600000000000,
'sub_industries': [
{'id': 2501, 'name': '通信设备', 'stock_count': 30},
{'id': 2502, 'name': '通信运营', 'stock_count': 15}
]
},
{
'id': 26,
'name': '电子',
'code': '801260',
'description': '半导体、电子制造、光学光电子',
'stock_count': 178,
'avg_change': 2.0,
'total_market_cap': 2000000000000,
'sub_industries': [
{'id': 2601, 'name': '半导体', 'stock_count': 45},
{'id': 2602, 'name': '电子制造', 'stock_count': 78},
{'id': 2603, 'name': '光学光电子', 'stock_count': 55}
]
},
{
'id': 27,
'name': '机械设备',
'code': '801270',
'description': '通用机械、专用设备、仪器仪表',
'stock_count': 234,
'avg_change': 1.1,
'total_market_cap': 1500000000000,
'sub_industries': [
{'id': 2701, 'name': '通用机械', 'stock_count': 89},
{'id': 2702, 'name': '专用设备', 'stock_count': 98},
{'id': 2703, 'name': '仪器仪表', 'stock_count': 47}
]
},
{
'id': 28,
'name': '食品饮料',
'code': '801280',
'description': '食品加工、饮料制造',
'stock_count': 67,
'avg_change': 1.3,
'total_market_cap': 1000000000000,
'sub_industries': [
{'id': 2801, 'name': '食品加工', 'stock_count': 35},
{'id': 2802, 'name': '饮料制造', 'stock_count': 32}
]
}
]
return jsonify({
'success': True,
'data': levels
})
except Exception as e:
print(f"Error getting industry levels: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/info', methods=['GET'])
def get_industry_info():
"""获取行业信息"""
try:
industry_id = request.args.get('industry_id')
if not industry_id:
return jsonify({'success': False, 'error': '请提供行业ID'}), 400
# 模拟行业信息
industry_info = {
'id': industry_id,
'name': f'行业{industry_id}',
'code': f'801{industry_id.zfill(3)}',
'description': f'这是行业{industry_id}的详细描述',
'stock_count': 50,
'avg_change': 1.5,
'total_market_cap': 800000000000,
'pe_ratio': 15.6,
'pb_ratio': 2.3,
'roe': 8.5,
'top_stocks': [
{'code': '000001', 'name': '龙头股A', 'weight': 0.15},
{'code': '000002', 'name': '龙头股B', 'weight': 0.12},
{'code': '000003', 'name': '龙头股C', 'weight': 0.10}
],
'sub_industries': [
{'id': 1, 'name': '子行业A', 'stock_count': 20},
{'id': 2, 'name': '子行业B', 'stock_count': 18},
{'id': 3, 'name': '子行业C', 'stock_count': 12}
],
'performance': {
'daily': 1.5,
'weekly': 3.2,
'monthly': 8.5,
'quarterly': 12.3,
'yearly': 25.6
},
'trend': {
'direction': 'up',
'strength': 'medium',
'duration': '3 months'
}
}
return jsonify({
'success': True,
'data': industry_info
})
except Exception as e:
print(f"Error getting industry info: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

469
app/routes/limitanalyse.py Normal file
View File

@@ -0,0 +1,469 @@
from flask import Blueprint, request, jsonify
import pandas as pd
import json
from datetime import datetime
bp = Blueprint('limitanalyse', __name__, url_prefix='/api/limit-analyse')
@bp.route('/available-dates', methods=['GET'])
def get_available_dates():
"""获取可用日期列表"""
try:
# 模拟可用日期
dates = [
'2025-07-16',
'2025-07-15',
'2025-07-14',
'2025-07-11',
'2025-07-10'
]
return jsonify({
'success': True,
'data': dates
})
except Exception as e:
print(f"Error getting available dates: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def load_stock_data(datestr):
"""加载股票数据"""
try:
# 模拟股票数据
data = []
for i in range(100):
data.append({
'code': f'00000{i:03d}',
'name': f'股票{i}',
'price': 10.0 + i * 0.1,
'change': (i % 10 - 5) * 0.5,
'sector': f'板块{i % 5}',
'limit_type': '涨停' if i % 10 == 0 else '正常',
'volume': 1000000 + i * 50000,
'amount': 10000000 + i * 500000
})
return pd.DataFrame(data)
except Exception as e:
print(f"Error loading stock data: {e}")
return pd.DataFrame()
@bp.route('/data', methods=['GET'])
def get_analysis_data():
"""获取分析数据"""
try:
date = request.args.get('date', '2025-07-16')
# 加载数据
df = load_stock_data(date)
if df.empty:
return jsonify({'success': False, 'error': '数据加载失败'}), 500
# 统计信息
total_stocks = len(df)
limit_up_stocks = len(df[df['limit_type'] == '涨停'])
limit_down_stocks = len(df[df['limit_type'] == '跌停'])
# 板块统计
sector_stats = df.groupby('sector').agg({
'code': 'count',
'change': 'mean',
'volume': 'sum'
}).reset_index()
sector_data = []
for _, row in sector_stats.iterrows():
sector_data.append({
'sector': row['sector'],
'stock_count': int(row['code']),
'avg_change': round(row['change'], 2),
'total_volume': int(row['volume'])
})
return jsonify({
'success': True,
'data': {
'date': date,
'total_stocks': total_stocks,
'limit_up_stocks': limit_up_stocks,
'limit_down_stocks': limit_down_stocks,
'sector_stats': sector_data
}
})
except Exception as e:
print(f"Error getting analysis data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/sector-data', methods=['GET'])
def get_sector_data():
"""获取板块数据"""
try:
date = request.args.get('date', '2025-07-16')
# 加载数据
df = load_stock_data(date)
if df.empty:
return jsonify({'success': False, 'error': '数据加载失败'}), 500
# 板块统计
sector_stats = df.groupby('sector').agg({
'code': 'count',
'change': 'mean',
'volume': 'sum',
'amount': 'sum'
}).reset_index()
sector_data = []
for _, row in sector_stats.iterrows():
sector_data.append({
'sector': row['sector'],
'stock_count': int(row['code']),
'avg_change': round(row['change'], 2),
'total_volume': int(row['volume']),
'total_amount': int(row['amount'])
})
return jsonify({
'success': True,
'data': sector_data
})
except Exception as e:
print(f"Error getting sector data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/word-cloud', methods=['GET'])
def get_word_cloud_data():
"""获取词云数据"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟词云数据
word_data = [
{'word': '科技', 'value': 100},
{'word': '新能源', 'value': 85},
{'word': '医药', 'value': 70},
{'word': '消费', 'value': 65},
{'word': '金融', 'value': 50},
{'word': '地产', 'value': 45},
{'word': '制造', 'value': 40},
{'word': '农业', 'value': 35},
{'word': '传媒', 'value': 30},
{'word': '环保', 'value': 25}
]
return jsonify({
'success': True,
'data': word_data
})
except Exception as e:
print(f"Error getting word cloud data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/chart-data', methods=['GET'])
def get_chart_data():
"""获取图表数据"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟图表数据
chart_data = {
'limit_up_distribution': [
{'sector': '科技', 'count': 15},
{'sector': '新能源', 'count': 12},
{'sector': '医药', 'count': 10},
{'sector': '消费', 'count': 8},
{'sector': '金融', 'count': 6}
],
'sector_performance': [
{'sector': '科技', 'change': 2.5},
{'sector': '新能源', 'change': 1.8},
{'sector': '医药', 'change': 1.2},
{'sector': '消费', 'change': 0.8},
{'sector': '金融', 'change': 0.5}
]
}
return jsonify({
'success': True,
'data': chart_data
})
except Exception as e:
print(f"Error getting chart data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/stock-details', methods=['GET'])
def get_stock_details():
"""获取股票详情"""
try:
code = request.args.get('code')
date = request.args.get('date', '2025-07-16')
if not code:
return jsonify({'success': False, 'error': '请提供股票代码'}), 400
# 模拟股票详情
stock_detail = {
'code': code,
'name': f'股票{code}',
'price': 15.50,
'change': 2.5,
'sector': '科技',
'volume': 1500000,
'amount': 23250000,
'limit_type': '涨停',
'turnover_rate': 3.2,
'market_cap': 15500000000
}
return jsonify({
'success': True,
'data': stock_detail
})
except Exception as e:
print(f"Error getting stock details: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/sector-analysis', methods=['GET'])
def get_sector_analysis():
"""获取板块分析"""
try:
sector = request.args.get('sector')
date = request.args.get('date', '2025-07-16')
if not sector:
return jsonify({'success': False, 'error': '请提供板块名称'}), 400
# 模拟板块分析数据
sector_analysis = {
'sector': sector,
'stock_count': 25,
'avg_change': 1.8,
'limit_up_count': 8,
'limit_down_count': 2,
'total_volume': 50000000,
'total_amount': 750000000,
'top_stocks': [
{'code': '000001', 'name': '股票A', 'change': 10.0},
{'code': '000002', 'name': '股票B', 'change': 9.5},
{'code': '000003', 'name': '股票C', 'change': 8.8}
]
}
return jsonify({
'success': True,
'data': sector_analysis
})
except Exception as e:
print(f"Error getting sector analysis: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/trend-analysis', methods=['GET'])
def get_trend_analysis():
"""获取趋势分析"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟趋势分析数据
trend_data = {
'limit_up_trend': [
{'date': '2025-07-10', 'count': 45},
{'date': '2025-07-11', 'count': 52},
{'date': '2025-07-14', 'count': 48},
{'date': '2025-07-15', 'count': 55},
{'date': '2025-07-16', 'count': 51}
],
'sector_trend': [
{'sector': '科技', 'trend': 'up'},
{'sector': '新能源', 'trend': 'up'},
{'sector': '医药', 'trend': 'stable'},
{'sector': '消费', 'trend': 'down'},
{'sector': '金融', 'trend': 'stable'}
]
}
return jsonify({
'success': True,
'data': trend_data
})
except Exception as e:
print(f"Error getting trend analysis: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/heat-map', methods=['GET'])
def get_heat_map_data():
"""获取热力图数据"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟热力图数据
heat_map_data = []
sectors = ['科技', '新能源', '医药', '消费', '金融', '地产', '制造', '农业']
for i, sector in enumerate(sectors):
for j in range(8):
heat_map_data.append({
'sector': sector,
'metric': f'指标{j+1}',
'value': (i + j) % 10 + 1
})
return jsonify({
'success': True,
'data': heat_map_data
})
except Exception as e:
print(f"Error getting heat map data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/correlation-analysis', methods=['GET'])
def get_correlation_analysis():
"""获取相关性分析"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟相关性分析数据
correlation_data = {
'sector_correlations': [
{'sector1': '科技', 'sector2': '新能源', 'correlation': 0.85},
{'sector1': '医药', 'sector2': '消费', 'correlation': 0.72},
{'sector1': '金融', 'sector2': '地产', 'correlation': 0.68},
{'sector1': '科技', 'sector2': '医药', 'correlation': 0.45},
{'sector1': '新能源', 'sector2': '制造', 'correlation': 0.78}
],
'stock_correlations': [
{'stock1': '000001', 'stock2': '000002', 'correlation': 0.92},
{'stock1': '000003', 'stock2': '000004', 'correlation': 0.88},
{'stock1': '000005', 'stock2': '000006', 'correlation': 0.76}
]
}
return jsonify({
'success': True,
'data': correlation_data
})
except Exception as e:
print(f"Error getting correlation analysis: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/export-data', methods=['POST'])
def export_data():
"""导出数据"""
try:
data = request.get_json()
date = data.get('date', '2025-07-16')
export_type = data.get('type', 'excel')
# 模拟导出
filename = f'limit_analyse_{date}.{export_type}'
return jsonify({
'success': True,
'message': '数据导出成功',
'data': {
'filename': filename,
'download_url': f'/downloads/{filename}'
}
})
except Exception as e:
print(f"Error exporting data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/high-position-stocks', methods=['GET'])
def get_high_position_stocks():
"""获取高位股统计数据"""
try:
date = request.args.get('date', datetime.now().strftime('%Y%m%d'))
# 模拟高位股数据 - 实际使用时需要连接真实的数据库
# 根据用户提供的表结构,查询连续涨停天数较多的股票
high_position_stocks = [
{
'stock_code': '000001',
'stock_name': '平安银行',
'price': 15.68,
'increase_rate': 10.02,
'limit_up_days': 5,
'continuous_limit_up': 3,
'industry': '银行',
'turnover_rate': 3.45,
'market_cap': 32000000000
},
{
'stock_code': '000002',
'stock_name': '万科A',
'price': 18.92,
'increase_rate': 9.98,
'limit_up_days': 4,
'continuous_limit_up': 2,
'industry': '房地产',
'turnover_rate': 5.67,
'market_cap': 21000000000
},
{
'stock_code': '600036',
'stock_name': '招商银行',
'price': 42.15,
'increase_rate': 8.45,
'limit_up_days': 6,
'continuous_limit_up': 4,
'industry': '银行',
'turnover_rate': 2.89,
'market_cap': 105000000000
},
{
'stock_code': '000858',
'stock_name': '五粮液',
'price': 168.50,
'increase_rate': 7.23,
'limit_up_days': 3,
'continuous_limit_up': 2,
'industry': '白酒',
'turnover_rate': 1.56,
'market_cap': 650000000000
},
{
'stock_code': '002415',
'stock_name': '海康威视',
'price': 35.68,
'increase_rate': 6.89,
'limit_up_days': 4,
'continuous_limit_up': 3,
'industry': '安防',
'turnover_rate': 4.12,
'market_cap': 33000000000
}
]
# 统计信息
total_count = len(high_position_stocks)
avg_continuous_days = sum(stock['continuous_limit_up'] for stock in high_position_stocks) / total_count if total_count > 0 else 0
# 按连续涨停天数排序
high_position_stocks.sort(key=lambda x: x['continuous_limit_up'], reverse=True)
return jsonify({
'success': True,
'data': {
'stocks': high_position_stocks,
'statistics': {
'total_count': total_count,
'avg_continuous_days': round(avg_continuous_days, 2),
'max_continuous_days': max([stock['continuous_limit_up'] for stock in high_position_stocks], default=0),
'industry_distribution': {}
}
}
})
except Exception as e:
print(f"Error getting high position stocks: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

241
app/routes/stocks.py Normal file
View File

@@ -0,0 +1,241 @@
from flask import Blueprint, request, jsonify
from app import db
from clickhouse_driver import Client
import pandas as pd
from datetime import datetime, timedelta
import pytz
bp = Blueprint('stocks', __name__, url_prefix='/api/stock')
def get_clickhouse_client():
"""获取ClickHouse客户端"""
return Client('localhost', port=9000, user='default', password='', database='default')
@bp.route('/quotes', methods=['GET', 'POST'])
def get_stock_quotes():
"""获取股票实时报价"""
try:
if request.method == 'GET':
# GET 请求从 URL 参数获取数据
codes = request.args.get('codes', '').split(',')
event_time_str = request.args.get('event_time')
else:
# POST 请求从 JSON 获取数据
codes = request.json.get('codes', [])
event_time_str = request.json.get('event_time')
if not codes:
return jsonify({'success': False, 'error': '请提供股票代码'}), 400
# 过滤空字符串
codes = [code.strip() for code in codes if code.strip()]
if not codes:
return jsonify({'success': False, 'error': '请提供有效的股票代码'}), 400
# 解析事件时间
event_time = None
if event_time_str:
try:
event_time = datetime.fromisoformat(event_time_str.replace('Z', '+00:00'))
except ValueError:
return jsonify({'success': False, 'error': '事件时间格式错误'}), 400
# 获取当前时间
now = datetime.now(pytz.timezone('Asia/Shanghai'))
# 如果提供了事件时间,使用事件时间;否则使用当前时间
target_time = event_time if event_time else now
# 获取交易日和交易时间
def get_trading_day_and_times(event_datetime):
"""获取交易日和交易时间列表"""
# 这里简化处理,实际应该查询交易日历
trading_day = event_datetime.strftime('%Y-%m-%d')
# 生成交易时间列表 (9:30-11:30, 13:00-15:00)
morning_times = [f"{trading_day} {hour:02d}:{minute:02d}"
for hour in range(9, 12)
for minute in range(0, 60, 1)
if not (hour == 9 and minute < 30) and not (hour == 11 and minute > 30)]
afternoon_times = [f"{trading_day} {hour:02d}:{minute:02d}"
for hour in range(13, 16)
for minute in range(0, 60, 1)]
return trading_day, morning_times + afternoon_times
trading_day, trading_times = get_trading_day_and_times(target_time)
# 模拟股票数据
results = {}
for code in codes:
# 这里应该从ClickHouse或其他数据源获取真实数据
# 现在使用模拟数据
import random
base_price = 10.0 + random.random() * 20.0
change = (random.random() - 0.5) * 2.0
results[code] = {
'price': round(base_price, 2),
'change': round(change, 2),
'name': f'股票{code}'
}
return jsonify({
'success': True,
'data': results
})
except Exception as e:
print(f"Error getting stock quotes: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<stock_code>/kline')
def get_stock_kline(stock_code):
"""获取股票K线数据"""
try:
chart_type = request.args.get('type', 'daily')
event_time_str = request.args.get('event_time')
if not event_time_str:
return jsonify({'success': False, 'error': '请提供事件时间'}), 400
try:
event_datetime = datetime.fromisoformat(event_time_str.replace('Z', '+00:00'))
except ValueError:
return jsonify({'success': False, 'error': '事件时间格式错误'}), 400
# 获取股票名称(这里简化处理)
stock_name = f'股票{stock_code}'
if chart_type == 'daily':
return get_daily_kline(stock_code, event_datetime, stock_name)
elif chart_type == 'minute':
return get_minute_kline(stock_code, event_datetime, stock_name)
elif chart_type == 'timeline':
return get_timeline_data(stock_code, event_datetime, stock_name)
else:
return jsonify({'error': f'Unsupported chart type: {chart_type}'}), 400
except Exception as e:
print(f"Error getting stock kline: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def get_daily_kline(stock_code, event_datetime, stock_name):
"""获取日K线数据"""
try:
# 模拟日K线数据
data = []
base_price = 10.0
for i in range(30):
date = (event_datetime - timedelta(days=30-i)).strftime('%Y-%m-%d')
open_price = base_price + (i * 0.1) + (i % 3 - 1) * 0.5
close_price = open_price + (i % 5 - 2) * 0.3
high_price = max(open_price, close_price) + 0.2
low_price = min(open_price, close_price) - 0.2
volume = 1000000 + i * 50000
data.append({
'date': date,
'open': round(open_price, 2),
'close': round(close_price, 2),
'high': round(high_price, 2),
'low': round(low_price, 2),
'volume': volume
})
return jsonify({
'code': stock_code,
'name': stock_name,
'trade_date': event_datetime.strftime('%Y-%m-%d'),
'data': data
})
except Exception as e:
print(f"Error getting daily kline: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def get_minute_kline(stock_code, event_datetime, stock_name):
"""获取分钟K线数据"""
try:
# 模拟分钟K线数据
data = []
base_price = 10.0
trading_times = []
# 生成交易时间
for hour in range(9, 16):
if hour == 12:
continue
for minute in range(0, 60):
if (hour == 9 and minute < 30) or (hour == 11 and minute > 30):
continue
trading_times.append(f"{hour:02d}:{minute:02d}")
for i, time in enumerate(trading_times):
open_price = base_price + (i * 0.01) + (i % 10 - 5) * 0.02
close_price = open_price + (i % 7 - 3) * 0.01
high_price = max(open_price, close_price) + 0.01
low_price = min(open_price, close_price) - 0.01
volume = 50000 + i * 1000
data.append({
'time': time,
'open': round(open_price, 2),
'close': round(close_price, 2),
'high': round(high_price, 2),
'low': round(low_price, 2),
'volume': volume
})
return jsonify({
'code': stock_code,
'name': stock_name,
'trade_date': event_datetime.strftime('%Y-%m-%d'),
'data': data
})
except Exception as e:
print(f"Error getting minute kline: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def get_timeline_data(stock_code, event_datetime, stock_name):
"""获取分时图数据"""
try:
# 模拟分时图数据
data = []
base_price = 10.0
trading_times = []
# 生成交易时间
for hour in range(9, 16):
if hour == 12:
continue
for minute in range(0, 60):
if (hour == 9 and minute < 30) or (hour == 11 and minute > 30):
continue
trading_times.append(f"{hour:02d}:{minute:02d}")
for i, time in enumerate(trading_times):
price = base_price + (i * 0.01) + (i % 10 - 5) * 0.02
avg_price = price + (i % 5 - 2) * 0.01
volume = 50000 + i * 1000
data.append({
'time': time,
'price': round(price, 2),
'avg_price': round(avg_price, 2),
'volume': volume
})
return jsonify({
'code': stock_code,
'name': stock_name,
'trade_date': event_datetime.strftime('%Y-%m-%d'),
'data': data
})
except Exception as e:
print(f"Error getting timeline data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500