Files
stock_cursor_v0/backend/event_driven.py
2026-06-14 11:54:45 +08:00

532 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""事件驱动策略 — 基于财经事件的量化交易。
功能:
1. 财报发布前后统计规律
2. 限售解禁影响回测
3. 高管增减持跟踪
4. 行业政策事件库
5. 事件驱动选股
"""
import datetime as dt
from typing import List, Dict, Any, Optional
from collections import defaultdict
import numpy as np
from sqlalchemy import select, and_, or_, func, desc
from db import get_session
from models import CorporateEvent, PolicyEvent, DailyQuote, StockMetric, Security
def collect_earnings_events(limit: int = 100) -> Dict[str, Any]:
"""采集财报事件(模拟数据)
实际生产需要接入:
- 东方财富财报日历
- 巨潮资讯网
- 新浪财经API
"""
# 模拟财报事件
with get_session() as s:
# 获取股票池
stocks = s.execute(
select(Security.code, Security.name).limit(limit)
).all()
saved = 0
for code, name in stocks:
# 模拟财报发布
event_date = dt.date.today() - dt.timedelta(days=np.random.randint(1, 90))
# 检查是否已存在
exists = s.execute(
select(CorporateEvent).where(
and_(
CorporateEvent.code == code,
CorporateEvent.event_type == 'earnings',
CorporateEvent.event_date == event_date
)
)
).scalar_one_or_none()
if not exists:
impact = np.random.choice(['positive', 'negative', 'neutral'], p=[0.4, 0.3, 0.3])
event = CorporateEvent(
code=code,
name=name,
event_type='earnings',
event_date=event_date,
title=f"{name}发布财报",
description=f"{'业绩超预期' if impact == 'positive' else ('业绩不及预期' if impact == 'negative' else '业绩符合预期')}",
impact=impact
)
s.add(event)
saved += 1
s.commit()
return {"ok": True, "saved": saved}
def analyze_earnings_pattern(days_before: int = 5, days_after: int = 10) -> Dict[str, Any]:
"""分析财报发布前后的股价规律
Args:
days_before: 财报前N天
days_after: 财报后N天
Returns:
统计结果
"""
with get_session() as s:
# 获取财报事件
events = s.execute(
select(CorporateEvent)
.where(CorporateEvent.event_type == 'earnings')
).scalars().all()
if not events:
return {"ok": False, "msg": "暂无财报数据"}
# 按影响分类
results = {
'positive': {'before': [], 'after': [], 'count': 0},
'negative': {'before': [], 'after': [], 'count': 0},
'neutral': {'before': [], 'after': [], 'count': 0}
}
for event in events:
# 获取前后股价
before_date = event.event_date - dt.timedelta(days=days_before)
after_date = event.event_date + dt.timedelta(days=days_after)
quotes = s.execute(
select(DailyQuote)
.where(
and_(
DailyQuote.code == event.code,
DailyQuote.date >= before_date,
DailyQuote.date <= after_date
)
)
.order_by(DailyQuote.date)
).scalars().all()
if len(quotes) < 2:
continue
# 找到事件日期的位置
event_idx = None
for i, q in enumerate(quotes):
if q.date >= event.event_date:
event_idx = i
break
if event_idx is None or event_idx == 0:
continue
# 计算前后收益
base_price = float(quotes[event_idx - 1].close)
# 事件前收益
if event_idx > 0:
before_return = (float(quotes[event_idx - 1].close) / float(quotes[0].close) - 1) * 100
results[event.impact]['before'].append(before_return)
# 事件后收益
if event_idx < len(quotes) - 1:
after_return = (float(quotes[-1].close) / base_price - 1) * 100
results[event.impact]['after'].append(after_return)
results[event.impact]['count'] += 1
# 计算统计指标
summary = {}
for impact, data in results.items():
if data['count'] > 0:
summary[impact] = {
'count': data['count'],
'avg_before': round(np.mean(data['before']), 2) if data['before'] else 0,
'avg_after': round(np.mean(data['after']), 2) if data['after'] else 0,
'win_rate_after': round(sum(1 for x in data['after'] if x > 0) / len(data['after']) * 100, 1) if data['after'] else 0
}
return {
"ok": True,
"days_before": days_before,
"days_after": days_after,
"summary": summary
}
def track_insider_trading(code: str = None, days: int = 180) -> Dict[str, Any]:
"""跟踪高管增减持
Args:
code: 股票代码None表示全部
days: 统计天数
Returns:
增减持记录
"""
since = dt.date.today() - dt.timedelta(days=days)
with get_session() as s:
query = select(CorporateEvent).where(
and_(
CorporateEvent.event_type == 'insider',
CorporateEvent.event_date >= since
)
)
if code:
query = query.where(CorporateEvent.code == code)
query = query.order_by(desc(CorporateEvent.event_date))
events = s.execute(query).scalars().all()
if not events:
return {"ok": False, "msg": "暂无增减持数据"}
# 统计
by_type = {'increase': [], 'decrease': []}
for e in events:
action = 'increase' if e.impact == 'positive' else 'decrease'
by_type[action].append({
'code': e.code,
'name': e.name,
'date': e.event_date.isoformat(),
'title': e.title,
'amount': e.amount
})
return {
"ok": True,
"days": days,
"total": len(events),
"increases": by_type['increase'],
"decreases": by_type['decrease']
}
def analyze_unlock_impact(days: int = 90) -> Dict[str, Any]:
"""分析限售解禁影响
Args:
days: 统计天数
Returns:
解禁影响统计
"""
since = dt.date.today() - dt.timedelta(days=days)
with get_session() as s:
events = s.execute(
select(CorporateEvent)
.where(
and_(
CorporateEvent.event_type == 'unlock',
CorporateEvent.event_date >= since
)
)
.order_by(CorporateEvent.event_date)
).scalars().all()
if not events:
return {"ok": False, "msg": "暂无解禁数据"}
results = []
for event in events:
# 获取解禁前后股价
before_date = event.event_date - dt.timedelta(days=10)
after_date = event.event_date + dt.timedelta(days=10)
quotes = s.execute(
select(DailyQuote)
.where(
and_(
DailyQuote.code == event.code,
DailyQuote.date >= before_date,
DailyQuote.date <= after_date
)
)
.order_by(DailyQuote.date)
).scalars().all()
if len(quotes) < 5:
continue
# 找到解禁日
unlock_idx = None
for i, q in enumerate(quotes):
if q.date >= event.event_date:
unlock_idx = i
break
if unlock_idx and unlock_idx > 0 and unlock_idx < len(quotes) - 1:
before_price = float(quotes[unlock_idx - 1].close)
after_price = float(quotes[-1].close)
impact_pct = (after_price / before_price - 1) * 100
results.append({
'code': event.code,
'name': event.name,
'date': event.event_date.isoformat(),
'amount': event.amount,
'impact_pct': round(impact_pct, 2),
'title': event.title
})
# 统计
if results:
avg_impact = np.mean([r['impact_pct'] for r in results])
negative_count = sum(1 for r in results if r['impact_pct'] < 0)
summary = {
'total': len(results),
'avg_impact': round(avg_impact, 2),
'negative_ratio': round(negative_count / len(results) * 100, 1)
}
else:
summary = {}
return {
"ok": True,
"days": days,
"summary": summary,
"events": results
}
def get_policy_events(sector: str = None, days: int = 180) -> Dict[str, Any]:
"""获取行业政策事件
Args:
sector: 板块名称None表示全部
days: 统计天数
Returns:
政策事件列表
"""
since = dt.date.today() - dt.timedelta(days=days)
with get_session() as s:
query = select(PolicyEvent).where(PolicyEvent.event_date >= since)
if sector:
query = query.where(PolicyEvent.sector == sector)
query = query.order_by(desc(PolicyEvent.event_date))
events = s.execute(query).scalars().all()
if not events:
return {"ok": False, "msg": "暂无政策数据"}
results = []
for e in events:
results.append({
'sector': e.sector,
'date': e.event_date.isoformat(),
'title': e.title,
'policy_type': e.policy_type,
'impact': e.impact,
'affected_stocks': e.affected_stocks.split(',') if e.affected_stocks else []
})
return {
"ok": True,
"days": days,
"total": len(results),
"events": results
}
def event_driven_selector(event_types: List[str], days: int = 30) -> Dict[str, Any]:
"""事件驱动选股
Args:
event_types: 事件类型列表,如 ['earnings_positive', 'insider_increase']
days: 最近N天的事件
Returns:
选股结果
"""
since = dt.date.today() - dt.timedelta(days=days)
with get_session() as s:
# 构建查询条件
conditions = []
for et in event_types:
if et == 'earnings_positive':
conditions.append(
and_(
CorporateEvent.event_type == 'earnings',
CorporateEvent.impact == 'positive'
)
)
elif et == 'insider_increase':
conditions.append(
and_(
CorporateEvent.event_type == 'insider',
CorporateEvent.impact == 'positive'
)
)
elif et == 'dividend':
conditions.append(CorporateEvent.event_type == 'dividend')
if not conditions:
return {"ok": False, "msg": "无效的事件类型"}
# 查询事件
query = select(CorporateEvent).where(
and_(
CorporateEvent.event_date >= since,
or_(*conditions)
)
).order_by(desc(CorporateEvent.event_date))
events = s.execute(query).scalars().all()
if not events:
return {"ok": False, "msg": "无符合条件的事件"}
# 按股票聚合
stock_events = defaultdict(list)
for e in events:
stock_events[e.code].append({
'type': e.event_type,
'date': e.event_date.isoformat(),
'impact': e.impact,
'title': e.title
})
# 获取股票最新数据
codes = list(stock_events.keys())
metrics = {}
for m in s.execute(
select(StockMetric).where(StockMetric.code.in_(codes))
).scalars():
metrics[m.code] = {
'name': m.name,
'close': m.close,
'pct': m.pct,
'ret20': m.ret20
}
# 构建结果
results = []
for code, evt_list in stock_events.items():
info = metrics.get(code, {'name': code, 'close': 0, 'pct': 0, 'ret20': 0})
results.append({
'code': code,
'name': info['name'],
'close': info['close'],
'pct': info['pct'],
'ret20': info['ret20'],
'events': evt_list,
'event_score': len(evt_list) # 事件数量作为评分
})
# 按事件评分排序
results.sort(key=lambda x: x['event_score'], reverse=True)
return {
"ok": True,
"days": days,
"event_types": event_types,
"count": len(results),
"stocks": results[:50]
}
def seed_sample_events():
"""生成示例事件数据(用于演示)"""
with get_session() as s:
# 获取股票池
stocks = s.execute(
select(Security.code, Security.name).limit(100)
).all()
saved = 0
for code, name in stocks:
# 随机生成不同类型的事件
event_types = [
('earnings', 'positive', f'{name}业绩超预期', 0),
('earnings', 'negative', f'{name}业绩不及预期', 0),
('insider', 'positive', f'{name}高管增持', np.random.uniform(0.1, 5)),
('insider', 'negative', f'{name}高管减持', np.random.uniform(0.1, 5)),
('unlock', 'negative', f'{name}限售解禁', np.random.uniform(1, 50)),
('dividend', 'positive', f'{name}分红派息', np.random.uniform(0.5, 3))
]
# 随机选择1-2个事件
selected = np.random.choice(len(event_types), size=min(2, len(event_types)), replace=False)
for idx in selected:
event_type, impact, title, amount = event_types[idx]
event_date = dt.date.today() - dt.timedelta(days=np.random.randint(1, 90))
# 检查是否已存在
exists = s.execute(
select(CorporateEvent).where(
and_(
CorporateEvent.code == code,
CorporateEvent.event_type == event_type,
CorporateEvent.event_date == event_date
)
)
).scalar_one_or_none()
if not exists:
event = CorporateEvent(
code=code,
name=name,
event_type=event_type,
event_date=event_date,
title=title,
amount=amount,
impact=impact
)
s.add(event)
saved += 1
# 生成政策事件
policies = [
('新能源', '新能源汽车补贴政策延续', 'subsidy', 'positive'),
('半导体', '芯片产业扶持政策出台', 'support', 'positive'),
('医药', '药品集采政策调整', 'regulation', 'negative'),
('光伏', '光伏补贴退坡', 'subsidy', 'negative'),
('人工智能', 'AI产业发展规划发布', 'support', 'positive'),
]
for sector, title, policy_type, impact in policies:
event_date = dt.date.today() - dt.timedelta(days=np.random.randint(1, 90))
exists = s.execute(
select(PolicyEvent).where(
and_(
PolicyEvent.sector == sector,
PolicyEvent.title == title
)
)
).scalar_one_or_none()
if not exists:
policy = PolicyEvent(
sector=sector,
event_date=event_date,
title=title,
policy_type=policy_type,
impact=impact
)
s.add(policy)
saved += 1
s.commit()
return {"ok": True, "saved": saved}