claude强化功能
This commit is contained in:
531
backend/event_driven.py
Normal file
531
backend/event_driven.py
Normal file
@@ -0,0 +1,531 @@
|
||||
"""事件驱动策略 — 基于财经事件的量化交易。
|
||||
|
||||
功能:
|
||||
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}
|
||||
Reference in New Issue
Block a user