首次提交
This commit is contained in:
commit
8b0eadc876
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
# .gitignore - MND-IA 项目
|
||||
|
||||
# 配置文件(包含敏感信息)
|
||||
config.json
|
||||
|
||||
# Python 缓存
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# 虚拟环境
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
|
||||
# 数据文件
|
||||
data/
|
||||
*.csv
|
||||
*.json.bak
|
||||
*.db
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# IDE 配置
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 测试覆盖率
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# 构建产物
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
137
CONFIG_GUIDE.md
Normal file
137
CONFIG_GUIDE.md
Normal file
@ -0,0 +1,137 @@
|
||||
# Configuration Example
|
||||
# 配置文件使用说明
|
||||
|
||||
这是 MND-IA 系统的配置文件模板。
|
||||
|
||||
## 配置项说明
|
||||
|
||||
### LLM 配置
|
||||
```json
|
||||
"llm": {
|
||||
"model_name": "gpt-4", // 模型名称(支持 gpt-4, gpt-3.5-turbo, glm-4, claude-3.5-sonnet 等)
|
||||
"api_base": "https://api.openai.com/v1", // API 地址
|
||||
"api_key": "your-api-key-here", // API Key(必填)
|
||||
"temperature": 0.7, // 生成温度(0-1)
|
||||
"max_tokens": 2000, // 最大生成长度
|
||||
"timeout": 60 // 超时时间(秒)
|
||||
}
|
||||
```
|
||||
|
||||
### 系统配置
|
||||
```json
|
||||
"system": {
|
||||
"data_dir": "data", // 数据存储目录
|
||||
"cache_dir": "data/news_cache", // 新闻缓存目录
|
||||
"log_level": "INFO" // 日志级别
|
||||
}
|
||||
```
|
||||
|
||||
### 爬虫配置
|
||||
```json
|
||||
"crawler": {
|
||||
"max_retries": 3, // 最大重试次数
|
||||
"retry_delay": 5, // 重试间隔(秒)
|
||||
"cache_days": 7 // 缓存保留天数
|
||||
}
|
||||
```
|
||||
|
||||
### 交易配置
|
||||
```json
|
||||
"trading": {
|
||||
"max_position_ratio": 0.2, // 单个资产最大仓位比例
|
||||
"risk_free_rate": 0.02 // 无风险利率
|
||||
}
|
||||
```
|
||||
|
||||
## 使用不同的 LLM 提供商
|
||||
|
||||
### OpenAI
|
||||
```json
|
||||
"llm": {
|
||||
"model_name": "gpt-4",
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
"api_key": "sk-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
### 智谱 GLM
|
||||
```json
|
||||
"llm": {
|
||||
"model_name": "glm-4",
|
||||
"api_base": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"api_key": "xxxxxxxxxxxx.xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
### Claude (Anthropic)
|
||||
```json
|
||||
"llm": {
|
||||
"model_name": "claude-3-5-sonnet-20241022",
|
||||
"api_base": "https://api.anthropic.com/v1",
|
||||
"api_key": "sk-ant-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
### 本地部署(Ollama)
|
||||
```json
|
||||
"llm": {
|
||||
"model_name": "qwen2.5:14b",
|
||||
"api_base": "http://localhost:11434/v1",
|
||||
"api_key": "ollama"
|
||||
}
|
||||
```
|
||||
|
||||
## 代码中使用配置
|
||||
|
||||
### 方式 1:直接访问配置属性
|
||||
```python
|
||||
from core.config import config
|
||||
|
||||
# 获取模型名称
|
||||
model = config.model_name
|
||||
|
||||
# 获取 API Key
|
||||
api_key = config.api_key
|
||||
|
||||
# 获取完整 LLM 配置
|
||||
llm_config = config.llm_config
|
||||
```
|
||||
|
||||
### 方式 2:使用统一的 LLM 调用接口
|
||||
```python
|
||||
from core.config import llm_call
|
||||
|
||||
# 直接调用 LLM
|
||||
response = llm_call(
|
||||
messages=[
|
||||
{"role": "system", "content": "你是 A 股分析师"},
|
||||
{"role": "user", "content": "分析这条新闻..."}
|
||||
],
|
||||
temperature=0.5 # 可以覆盖配置文件中的默认值
|
||||
)
|
||||
```
|
||||
|
||||
### 方式 3:获取配置好的客户端
|
||||
```python
|
||||
from core.config import get_llm_client
|
||||
|
||||
client = get_llm_client()
|
||||
if client:
|
||||
response = client.chat.completions.create(...)
|
||||
```
|
||||
|
||||
## 环境变量支持(TODO)
|
||||
|
||||
未来版本将支持环境变量覆盖:
|
||||
```bash
|
||||
export MND_IA_API_KEY="sk-xxxxxxxxxxxx"
|
||||
export MND_IA_MODEL_NAME="gpt-4"
|
||||
```
|
||||
|
||||
## 安全提示
|
||||
|
||||
⚠️ **重要**:
|
||||
1. 不要将包含真实 API Key 的 `config.json` 提交到 Git
|
||||
2. 建议将 `config.json` 添加到 `.gitignore`
|
||||
3. 团队协作时,提供 `config.example.json` 模板
|
||||
4. 生产环境使用环境变量或密钥管理服务
|
||||
149
QUICKSTART.md
Normal file
149
QUICKSTART.md
Normal file
@ -0,0 +1,149 @@
|
||||
# 🚀 MND-IA 快速开始指南
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**核心依赖说明**:
|
||||
- `akshare`: A股行情数据获取
|
||||
- `cloudscraper`: 绕过 Cloudflare 防护的爬虫工具
|
||||
- `beautifulsoup4`: HTML 解析
|
||||
- `pandas`, `numpy`: 数据处理
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 1. 测试核心模块(World Book)
|
||||
|
||||
```bash
|
||||
python core/world_book.py
|
||||
```
|
||||
|
||||
### 2. 测试 Agent Skills
|
||||
|
||||
```bash
|
||||
# 测试量化模块
|
||||
python skills/quant.py
|
||||
|
||||
# 测试情报分析
|
||||
python skills/analyst.py
|
||||
|
||||
# 测试策略师
|
||||
python skills/strategist.py
|
||||
|
||||
# 测试基金经理
|
||||
python skills/pm.py
|
||||
|
||||
# 测试审计员
|
||||
python skills/auditor.py
|
||||
|
||||
# 测试新闻联播爬虫
|
||||
python skills/crawler.py
|
||||
```
|
||||
|
||||
### 3. 运行完整系统
|
||||
|
||||
```bash
|
||||
# 测试模式(使用模拟数据)
|
||||
python main.py --mode test --no-crawler
|
||||
|
||||
# 测试模式(使用爬虫抓取新闻联播)
|
||||
python main.py --mode test
|
||||
|
||||
# 每日批处理模式(自动爬取新闻)
|
||||
python main.py --mode daily --capital 1000000
|
||||
|
||||
# 盘后审计模式
|
||||
python main.py --mode audit
|
||||
```
|
||||
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
MND-IA/
|
||||
├── core/ # 共享内核
|
||||
│ ├── world_book.py # WorldBook 核心类
|
||||
│ ├── asset_map.json # 资产映射表
|
||||
│ └── macro_matrix.json # 宏观因子矩阵
|
||||
├── skills/ # Agent 技能群
|
||||
│ ├── analyst.py # Skill A: 情报分析师
|
||||
│ ├── quant.py # Skill B: 量化研究员
|
||||
│ ├── strategist.py # Skill C: 宏观策略师
|
||||
│ ├── pm.py # Skill D: 基金经理
|
||||
│ ├── auditor.py # Skill E: 复盘审计员
|
||||
│ └── crawler.py # Skill F: 新闻联播爬虫
|
||||
├── data/ # 数据存储
|
||||
├── logs/ # 日志文件
|
||||
└── main.py # 主调度程序
|
||||
```
|
||||
|
||||
## 数据流示意
|
||||
|
||||
```
|
||||
[新闻联播] ──→ Crawler (Skill F) ──→ 新闻文本
|
||||
↓
|
||||
Analyst ──→ Narrative_JSON
|
||||
↓
|
||||
[行情] ──→ Quant ──→ Market_Data ──→ Strategist ──→ WorldBook
|
||||
↓
|
||||
PM ──→ Trust Index ──→ Orders
|
||||
↑
|
||||
Auditor (复盘)
|
||||
```
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Trust Index 公式
|
||||
|
||||
```
|
||||
TrustIndex = (Score_narrative × 0.6 + Score_flow × 0.4) - Penalty
|
||||
|
||||
一票否决:
|
||||
if Narrative > 80 and Flow < 30:
|
||||
Penalty = 100
|
||||
```
|
||||
|
||||
### 叙事生命周期
|
||||
|
||||
1. **incubation (潜伏期)**: 新闻刚出现,市场未反应
|
||||
2. **fermentation (发酵期)**: 政策出台,市场开始关注
|
||||
3. **realization (兑现期)**: Level A 重大利好,资金涌入
|
||||
4. **decay (衰退期)**: 热度下降,逐步退出
|
||||
|
||||
## 下一步
|
||||
|
||||
1. ~~配置新闻数据源(可接入财联社、东方财富等 API)~~ ✅ 已实现新闻联播爬虫
|
||||
2. 配置 LLM(如 GLM-4 或 Claude-3.5)用于 Analyst 增强
|
||||
3. 实现实盘交易接口(如东方财富、同花顺)
|
||||
4. 部署定时任务(每日 08:30 自动执行)
|
||||
5. 添加更多新闻源(如财联社快讯、部委官网等)
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: akshare 获取数据失败?**
|
||||
A: 确保网络连接正常,某些 API 有频率限制。
|
||||
|
||||
**Q: 爬虫无法获取新闻联播?**
|
||||
A:
|
||||
1. 检查网站是否可访问(可能被墙或维护)
|
||||
2. 查看 `data/news_cache/` 目录下的缓存文件
|
||||
3. 手动测试:`python skills/crawler.py`
|
||||
4. 新闻联播通常 20:00 后更新,提前爬取会返回空
|
||||
|
||||
**Q: cloudscraper 安装失败?**
|
||||
A:
|
||||
```bash
|
||||
pip install --upgrade pip
|
||||
pip install cloudscraper requests[security] pyparsing
|
||||
```
|
||||
|
||||
**Q: 如何添加新的 ETF?**
|
||||
A: 编辑 `core/asset_map.json`,在对应板块添加 ETF 代码。
|
||||
|
||||
**Q: 如何调整 Trust Index 公式权重?**
|
||||
A: 修改 `skills/pm.py` 中的 `calculate_trust_index()` 方法。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
16
core/__init__.py
Normal file
16
core/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
MND-IA Core Module
|
||||
==================
|
||||
共享内核 - 系统数据中心和逻辑基石
|
||||
"""
|
||||
|
||||
from .world_book import WorldBook, Narrative, MacroCycle, create_narrative_id
|
||||
|
||||
__all__ = [
|
||||
'WorldBook',
|
||||
'Narrative',
|
||||
'MacroCycle',
|
||||
'create_narrative_id'
|
||||
]
|
||||
|
||||
__version__ = '1.0.0'
|
||||
155
core/asset_map.json
Normal file
155
core/asset_map.json
Normal file
@ -0,0 +1,155 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": "4.0.0",
|
||||
"description": "Agent专用资产映射表。包含资产代码、关键词触发器及宏观因子敏感度矩阵。",
|
||||
"instruction": "当计算Trust Index时,使用sensitivity中的数值作为基础权重(x),结合Narrative强度(y)进行最终打分。"
|
||||
},
|
||||
|
||||
"assets": {
|
||||
"core_A500": {
|
||||
"name": "中证A500",
|
||||
"etfs": ["512050", "512020", "563360"],
|
||||
"keywords": ["核心资产", "大盘", "指数增强", "国家队", "长线资金"],
|
||||
"sensitivity": {
|
||||
"liquidity_easing": 0.6,
|
||||
"policy_support": 0.8,
|
||||
"cpi_rebound": 0.5,
|
||||
"risk_on": 0.9
|
||||
}
|
||||
},
|
||||
"tech_hardware": {
|
||||
"name": "半导体与硬科技",
|
||||
"etfs": ["512480", "588000"],
|
||||
"keywords": ["芯片", "光刻机", "集成电路", "卡脖子", "科创板"],
|
||||
"sensitivity": {
|
||||
"interest_rate_down": 0.9,
|
||||
"policy_tech_self_reliance": 1.0,
|
||||
"geopolitics_tension": 0.7,
|
||||
"risk_on": 1.0
|
||||
}
|
||||
},
|
||||
"tech_software": {
|
||||
"name": "软件与数字经济",
|
||||
"etfs": ["515230", "159869"],
|
||||
"keywords": ["信创", "国产软件", "操作系统", "数据要素", "AI应用", "网络安全"],
|
||||
"sensitivity": {
|
||||
"policy_digital_economy": 1.0,
|
||||
"govt_spending": 0.8,
|
||||
"risk_on": 0.9,
|
||||
"interest_rate_down": 0.7
|
||||
}
|
||||
},
|
||||
"tech_infra": {
|
||||
"name": "5G与新基建",
|
||||
"etfs": ["515050", "515880"],
|
||||
"keywords": ["5G", "6G", "光模块", "通信基站", "算力底座", "东数西算"],
|
||||
"sensitivity": {
|
||||
"policy_new_infra": 1.0,
|
||||
"tech_cycle_up": 0.8,
|
||||
"export_growth": 0.6
|
||||
}
|
||||
},
|
||||
"finance_broker": {
|
||||
"name": "券商/牛市旗手",
|
||||
"etfs": ["512880", "512900"],
|
||||
"keywords": ["成交量", "资本市场", "印花税", "T+0", "金融强国"],
|
||||
"sensitivity": {
|
||||
"market_volume_spike": 1.0,
|
||||
"policy_capital_market": 1.0,
|
||||
"liquidity_easing": 0.8
|
||||
}
|
||||
},
|
||||
"finance_bank": {
|
||||
"name": "银行/高股息",
|
||||
"etfs": ["512800", "159887"],
|
||||
"keywords": ["高股息", "中特估", "红利", "避险", "存款利率"],
|
||||
"sensitivity": {
|
||||
"interest_rate_down": -0.5,
|
||||
"risk_off": 0.8,
|
||||
"policy_soe_reform": 0.7
|
||||
}
|
||||
},
|
||||
"new_energy_vehicle": {
|
||||
"name": "新能源车与电池",
|
||||
"etfs": ["515030", "159840"],
|
||||
"keywords": ["电动车", "锂电池", "宁德时代", "汽车下乡", "碳中和"],
|
||||
"sensitivity": {
|
||||
"consumption_stimulus": 0.8,
|
||||
"export_growth": 0.9,
|
||||
"oil_price_up": 0.5
|
||||
}
|
||||
},
|
||||
"consumption_staples": {
|
||||
"name": "主要消费(白酒)",
|
||||
"etfs": ["512690"],
|
||||
"keywords": ["白酒", "内需", "CPI", "春节", "高端消费"],
|
||||
"sensitivity": {
|
||||
"cpi_rebound": 0.9,
|
||||
"liquidity_easing": 0.6,
|
||||
"foreign_inflow": 0.8
|
||||
}
|
||||
},
|
||||
"healthcare": {
|
||||
"name": "医药医疗",
|
||||
"etfs": ["512010", "159898"],
|
||||
"keywords": ["创新药", "医疗器械", "老龄化", "医保", "CXO"],
|
||||
"sensitivity": {
|
||||
"interest_rate_down": 0.8,
|
||||
"policy_public_health": 0.7,
|
||||
"demographic_trend": 0.5
|
||||
}
|
||||
},
|
||||
"cross_border_tech": {
|
||||
"name": "跨境科技(T+0)",
|
||||
"etfs": ["513330", "513300", "513500"],
|
||||
"keywords": ["港股", "美股", "纳斯达克", "中概股", "平台经济"],
|
||||
"sensitivity": {
|
||||
"fed_rate_cut": 0.9,
|
||||
"currency_rmb_depreciation": 0.8,
|
||||
"policy_platform_economy": 0.7
|
||||
}
|
||||
},
|
||||
"agriculture": {
|
||||
"name": "农业与粮食安全",
|
||||
"etfs": ["159825"],
|
||||
"keywords": ["粮食安全", "一号文件", "猪周期", "种业", "食品通胀"],
|
||||
"sensitivity": {
|
||||
"cpi_rebound": 0.8,
|
||||
"policy_food_security": 1.0,
|
||||
"geopolitics_tension": 0.6
|
||||
}
|
||||
},
|
||||
"resources_gold": {
|
||||
"name": "资源与黄金",
|
||||
"etfs": ["518880", "512400"],
|
||||
"keywords": ["黄金", "有色", "通胀", "避险", "美元下跌"],
|
||||
"sensitivity": {
|
||||
"dollar_index_down": 0.9,
|
||||
"geopolitics_tension": 1.0,
|
||||
"inflation_expectations": 0.8
|
||||
}
|
||||
},
|
||||
"defense_low_altitude": {
|
||||
"name": "军工与低空",
|
||||
"etfs": ["512660"],
|
||||
"keywords": ["低空经济", "国防", "军费", "无人机", "地缘"],
|
||||
"sensitivity": {
|
||||
"policy_low_altitude": 1.0,
|
||||
"geopolitics_tension": 0.9,
|
||||
"govt_spending": 0.7
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"macro_factors_definition": {
|
||||
"liquidity_easing": "央行降准降息,或市场资金利率下行",
|
||||
"interest_rate_down": "无风险利率下行(利好成长股)",
|
||||
"cpi_rebound": "通胀回升,消费复苏",
|
||||
"risk_on": "市场情绪高涨,追逐高风险高收益",
|
||||
"risk_off": "市场恐慌,资金流向避险资产",
|
||||
"policy_tech_self_reliance": "政策强调自主可控、国产替代",
|
||||
"geopolitics_tension": "外部冲突、制裁或地缘紧张",
|
||||
"currency_rmb_depreciation": "人民币贬值(利好出口及跨境资产)",
|
||||
"fed_rate_cut": "美联储降息(利好港股/美股/黄金)"
|
||||
}
|
||||
}
|
||||
219
core/config.py
Normal file
219
core/config.py
Normal file
@ -0,0 +1,219 @@
|
||||
"""
|
||||
配置管理模块
|
||||
提供统一的配置加载和访问接口,支持大模型配置、系统配置等
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class Config:
|
||||
"""配置管理类,单例模式"""
|
||||
|
||||
_instance = None
|
||||
_config_data: Dict[str, Any] = {}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not self._config_data:
|
||||
self.load_config()
|
||||
|
||||
def load_config(self, config_path: Optional[str] = None):
|
||||
"""
|
||||
加载配置文件
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径,默认为项目根目录下的 config.json
|
||||
"""
|
||||
if config_path is None:
|
||||
# 获取项目根目录(core 目录的上级目录)
|
||||
project_root = Path(__file__).parent.parent
|
||||
config_path = project_root / "config.json"
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
raise FileNotFoundError(f"配置文件不存在: {config_path}")
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
self._config_data = json.load(f)
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
获取配置项(支持点号分隔的多级key)
|
||||
|
||||
Args:
|
||||
key: 配置项键,如 "llm.model_name" 或 "llm"
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
配置值
|
||||
"""
|
||||
keys = key.split('.')
|
||||
value = self._config_data
|
||||
|
||||
for k in keys:
|
||||
if isinstance(value, dict):
|
||||
value = value.get(k)
|
||||
if value is None:
|
||||
return default
|
||||
else:
|
||||
return default
|
||||
|
||||
return value
|
||||
|
||||
@property
|
||||
def llm_config(self) -> Dict[str, Any]:
|
||||
"""获取大模型配置"""
|
||||
return self.get('llm', {})
|
||||
|
||||
@property
|
||||
def model_name(self) -> str:
|
||||
"""获取模型名称"""
|
||||
return self.get('llm.model_name', 'gpt-4')
|
||||
|
||||
@property
|
||||
def api_base(self) -> str:
|
||||
"""获取API地址"""
|
||||
return self.get('llm.api_base', 'https://api.openai.com/v1')
|
||||
|
||||
@property
|
||||
def api_key(self) -> str:
|
||||
"""获取API Key"""
|
||||
return self.get('llm.api_key', '')
|
||||
|
||||
@property
|
||||
def temperature(self) -> float:
|
||||
"""获取温度参数"""
|
||||
return self.get('llm.temperature', 0.7)
|
||||
|
||||
@property
|
||||
def max_tokens(self) -> int:
|
||||
"""获取最大token数"""
|
||||
return self.get('llm.max_tokens', 2000)
|
||||
|
||||
@property
|
||||
def timeout(self) -> int:
|
||||
"""获取超时时间"""
|
||||
return self.get('llm.timeout', 60)
|
||||
|
||||
def update(self, key: str, value: Any):
|
||||
"""
|
||||
更新配置项
|
||||
|
||||
Args:
|
||||
key: 配置项键,支持点号分隔的多级key
|
||||
value: 新值
|
||||
"""
|
||||
keys = key.split('.')
|
||||
config = self._config_data
|
||||
|
||||
for k in keys[:-1]:
|
||||
if k not in config:
|
||||
config[k] = {}
|
||||
config = config[k]
|
||||
|
||||
config[keys[-1]] = value
|
||||
|
||||
def save_config(self, config_path: Optional[str] = None):
|
||||
"""
|
||||
保存配置到文件
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径,默认为项目根目录下的 config.json
|
||||
"""
|
||||
if config_path is None:
|
||||
project_root = Path(__file__).parent.parent
|
||||
config_path = project_root / "config.json"
|
||||
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self._config_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# 全局配置实例
|
||||
config = Config()
|
||||
|
||||
|
||||
def get_llm_client():
|
||||
"""
|
||||
获取配置好的LLM客户端(OpenAI SDK)
|
||||
|
||||
Returns:
|
||||
OpenAI客户端实例
|
||||
"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=config.api_key,
|
||||
base_url=config.api_base,
|
||||
timeout=config.timeout
|
||||
)
|
||||
return client
|
||||
except ImportError:
|
||||
print("警告: openai 库未安装,无法创建LLM客户端")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"创建LLM客户端失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def llm_call(messages: list, **kwargs) -> Optional[str]:
|
||||
"""
|
||||
统一的LLM调用接口
|
||||
|
||||
Args:
|
||||
messages: 消息列表,格式为 [{"role": "user", "content": "..."}]
|
||||
**kwargs: 其他参数(会覆盖配置文件中的默认值)
|
||||
|
||||
Returns:
|
||||
LLM返回的文本内容,失败返回None
|
||||
"""
|
||||
client = get_llm_client()
|
||||
if client is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 合并配置和传入的参数
|
||||
params = {
|
||||
'model': config.model_name,
|
||||
'temperature': config.temperature,
|
||||
'max_tokens': config.max_tokens,
|
||||
}
|
||||
params.update(kwargs)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
messages=messages,
|
||||
**params
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
except Exception as e:
|
||||
print(f"LLM调用失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试配置加载
|
||||
print("=== 配置管理模块测试 ===")
|
||||
print(f"模型名称: {config.model_name}")
|
||||
print(f"API地址: {config.api_base}")
|
||||
print(f"API Key: {config.api_key[:10]}..." if config.api_key else "API Key: 未配置")
|
||||
print(f"温度: {config.temperature}")
|
||||
print(f"最大tokens: {config.max_tokens}")
|
||||
|
||||
print("\n完整LLM配置:")
|
||||
print(json.dumps(config.llm_config, indent=2, ensure_ascii=False))
|
||||
|
||||
# 测试配置更新
|
||||
print("\n测试配置更新...")
|
||||
config.update('llm.temperature', 0.5)
|
||||
print(f"更新后的温度: {config.temperature}")
|
||||
print()
|
||||
# 测试获取不存在的配置
|
||||
print(f"\n获取不存在的配置: {config.get('nonexistent.key', 'default_value')}")
|
||||
# 测试调用
|
||||
respodse = llm_call([{"role": "user", "content": "你好,用100字介绍一下你自己。"}])
|
||||
print(f"\nLLM调用结果:\n{respodse}")
|
||||
243
core/macro_matrix.json
Normal file
243
core/macro_matrix.json
Normal file
@ -0,0 +1,243 @@
|
||||
{
|
||||
"_comment": "宏观逻辑矩阵 - 定义宏观因子对各 ETF 板块的基础理论影响值",
|
||||
"_version": "1.0.0",
|
||||
"_last_updated": "2025-12-30",
|
||||
"_scoring_rules": {
|
||||
"range": "影响值范围: -10 (极度负面) 到 +10 (极度正面)",
|
||||
"neutral": "0 表示无明显影响",
|
||||
"interpretation": "数值越大表示正面影响越强,数值越小表示负面影响越强"
|
||||
},
|
||||
|
||||
"macro_factors": {
|
||||
"rate_cut": {
|
||||
"description": "降准降息 - 货币宽松政策",
|
||||
"impact": {
|
||||
"tech_etf": 8,
|
||||
"ai_etf": 8,
|
||||
"semiconductor_etf": 7,
|
||||
"new_energy_vehicle_etf": 7,
|
||||
"solar_etf": 6,
|
||||
"pharma_etf": 5,
|
||||
"consumer_etf": 7,
|
||||
"bank_etf": -5,
|
||||
"insurance_etf": -4,
|
||||
"broker_etf": 6,
|
||||
"realestate_etf": 9,
|
||||
"military_etf": 4,
|
||||
"metal_etf": 5,
|
||||
"coal_etf": 2
|
||||
}
|
||||
},
|
||||
"rate_hike": {
|
||||
"description": "加息收紧 - 货币紧缩政策",
|
||||
"impact": {
|
||||
"tech_etf": -7,
|
||||
"ai_etf": -7,
|
||||
"semiconductor_etf": -6,
|
||||
"new_energy_vehicle_etf": -6,
|
||||
"solar_etf": -5,
|
||||
"pharma_etf": -3,
|
||||
"consumer_etf": -6,
|
||||
"bank_etf": 5,
|
||||
"insurance_etf": 4,
|
||||
"broker_etf": -4,
|
||||
"realestate_etf": -9,
|
||||
"military_etf": -2,
|
||||
"metal_etf": -4,
|
||||
"coal_etf": -1
|
||||
}
|
||||
},
|
||||
"currency_depreciation": {
|
||||
"description": "人民币贬值 - 有利出口型企业",
|
||||
"impact": {
|
||||
"tech_etf": 3,
|
||||
"ai_etf": 1,
|
||||
"semiconductor_etf": 2,
|
||||
"new_energy_vehicle_etf": 5,
|
||||
"solar_etf": 6,
|
||||
"pharma_etf": 2,
|
||||
"consumer_etf": -3,
|
||||
"bank_etf": -2,
|
||||
"insurance_etf": -1,
|
||||
"broker_etf": -1,
|
||||
"realestate_etf": -4,
|
||||
"military_etf": 3,
|
||||
"metal_etf": 4,
|
||||
"coal_etf": 1
|
||||
}
|
||||
},
|
||||
"currency_appreciation": {
|
||||
"description": "人民币升值 - 有利进口型企业",
|
||||
"impact": {
|
||||
"tech_etf": -2,
|
||||
"ai_etf": -1,
|
||||
"semiconductor_etf": -1,
|
||||
"new_energy_vehicle_etf": -4,
|
||||
"solar_etf": -5,
|
||||
"pharma_etf": -1,
|
||||
"consumer_etf": 4,
|
||||
"bank_etf": 3,
|
||||
"insurance_etf": 2,
|
||||
"broker_etf": 2,
|
||||
"realestate_etf": 5,
|
||||
"military_etf": -2,
|
||||
"metal_etf": -3,
|
||||
"coal_etf": -1
|
||||
}
|
||||
},
|
||||
"fiscal_stimulus": {
|
||||
"description": "财政刺激政策 - 基建、消费刺激",
|
||||
"impact": {
|
||||
"tech_etf": 6,
|
||||
"ai_etf": 7,
|
||||
"semiconductor_etf": 6,
|
||||
"new_energy_vehicle_etf": 8,
|
||||
"solar_etf": 7,
|
||||
"pharma_etf": 4,
|
||||
"consumer_etf": 9,
|
||||
"bank_etf": 5,
|
||||
"insurance_etf": 4,
|
||||
"broker_etf": 7,
|
||||
"realestate_etf": 8,
|
||||
"military_etf": 6,
|
||||
"metal_etf": 8,
|
||||
"coal_etf": 6
|
||||
}
|
||||
},
|
||||
"regulation_tightening": {
|
||||
"description": "监管收紧 - 行业整顿",
|
||||
"impact": {
|
||||
"tech_etf": -6,
|
||||
"ai_etf": -4,
|
||||
"semiconductor_etf": -3,
|
||||
"new_energy_vehicle_etf": -4,
|
||||
"solar_etf": -3,
|
||||
"pharma_etf": -7,
|
||||
"consumer_etf": -5,
|
||||
"bank_etf": -2,
|
||||
"insurance_etf": -2,
|
||||
"broker_etf": -5,
|
||||
"realestate_etf": -8,
|
||||
"military_etf": -1,
|
||||
"metal_etf": -2,
|
||||
"coal_etf": -3
|
||||
}
|
||||
},
|
||||
"export_strong": {
|
||||
"description": "出口数据强劲",
|
||||
"impact": {
|
||||
"tech_etf": 5,
|
||||
"ai_etf": 3,
|
||||
"semiconductor_etf": 4,
|
||||
"new_energy_vehicle_etf": 7,
|
||||
"solar_etf": 8,
|
||||
"pharma_etf": 3,
|
||||
"consumer_etf": 2,
|
||||
"bank_etf": 3,
|
||||
"insurance_etf": 2,
|
||||
"broker_etf": 4,
|
||||
"realestate_etf": 1,
|
||||
"military_etf": 4,
|
||||
"metal_etf": 6,
|
||||
"coal_etf": 3
|
||||
}
|
||||
},
|
||||
"pmi_expansion": {
|
||||
"description": "PMI 扩张 (>50) - 制造业景气",
|
||||
"impact": {
|
||||
"tech_etf": 7,
|
||||
"ai_etf": 6,
|
||||
"semiconductor_etf": 7,
|
||||
"new_energy_vehicle_etf": 8,
|
||||
"solar_etf": 7,
|
||||
"pharma_etf": 4,
|
||||
"consumer_etf": 6,
|
||||
"bank_etf": 5,
|
||||
"insurance_etf": 4,
|
||||
"broker_etf": 6,
|
||||
"realestate_etf": 4,
|
||||
"military_etf": 6,
|
||||
"metal_etf": 8,
|
||||
"coal_etf": 7
|
||||
}
|
||||
},
|
||||
"oil_price_surge": {
|
||||
"description": "原油价格大涨",
|
||||
"impact": {
|
||||
"tech_etf": -3,
|
||||
"ai_etf": -2,
|
||||
"semiconductor_etf": -2,
|
||||
"new_energy_vehicle_etf": 5,
|
||||
"solar_etf": 6,
|
||||
"pharma_etf": -1,
|
||||
"consumer_etf": -4,
|
||||
"bank_etf": -1,
|
||||
"insurance_etf": -1,
|
||||
"broker_etf": 0,
|
||||
"realestate_etf": -3,
|
||||
"military_etf": -1,
|
||||
"metal_etf": 3,
|
||||
"coal_etf": 8
|
||||
}
|
||||
},
|
||||
"us_tech_weakness": {
|
||||
"description": "美国科技股大跌",
|
||||
"impact": {
|
||||
"tech_etf": -6,
|
||||
"ai_etf": -7,
|
||||
"semiconductor_etf": -8,
|
||||
"new_energy_vehicle_etf": -4,
|
||||
"solar_etf": -3,
|
||||
"pharma_etf": -2,
|
||||
"consumer_etf": -3,
|
||||
"bank_etf": 2,
|
||||
"insurance_etf": 1,
|
||||
"broker_etf": -2,
|
||||
"realestate_etf": 1,
|
||||
"military_etf": 0,
|
||||
"metal_etf": 0,
|
||||
"coal_etf": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"etf_mapping": {
|
||||
"tech_etf": ["515980", "159813"],
|
||||
"ai_etf": ["515980", "159813"],
|
||||
"semiconductor_etf": ["512480", "159995"],
|
||||
"new_energy_vehicle_etf": ["515030", "159806"],
|
||||
"solar_etf": ["515790", "159857"],
|
||||
"pharma_etf": ["512010", "159938"],
|
||||
"consumer_etf": ["159928", "510630"],
|
||||
"bank_etf": ["510230"],
|
||||
"insurance_etf": ["512880"],
|
||||
"broker_etf": ["512000"],
|
||||
"realestate_etf": ["512200", "159707"],
|
||||
"military_etf": ["512660", "512810"],
|
||||
"metal_etf": ["512400", "159881"],
|
||||
"coal_etf": ["515220", "159678"]
|
||||
},
|
||||
|
||||
"composite_scenarios": {
|
||||
"bull_market_start": {
|
||||
"description": "牛市启动期 - 货币宽松 + 政策利好",
|
||||
"factors": ["rate_cut", "fiscal_stimulus"],
|
||||
"multiplier": 1.2
|
||||
},
|
||||
"bear_market_bottom": {
|
||||
"description": "熊市底部 - 政策转向前",
|
||||
"factors": ["rate_hike", "regulation_tightening"],
|
||||
"multiplier": 0.8
|
||||
},
|
||||
"economic_recovery": {
|
||||
"description": "经济复苏期",
|
||||
"factors": ["pmi_expansion", "export_strong", "fiscal_stimulus"],
|
||||
"multiplier": 1.15
|
||||
},
|
||||
"risk_off": {
|
||||
"description": "风险规避模式",
|
||||
"factors": ["us_tech_weakness", "currency_depreciation"],
|
||||
"multiplier": 0.9
|
||||
}
|
||||
}
|
||||
}
|
||||
400
core/world_book.py
Normal file
400
core/world_book.py
Normal file
@ -0,0 +1,400 @@
|
||||
"""
|
||||
MND-IA Core: World Book (世界书)
|
||||
====================================
|
||||
这是系统的数据中心和逻辑基石,存储宏观状态、活跃叙事对象和历史经验。
|
||||
所有 Agent 必须通过此模块访问和更新系统状态。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
class Narrative:
|
||||
"""单个叙事对象 (Narrative Object)"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
topic: str,
|
||||
related_etfs: List[str],
|
||||
lifecycle_stage: str = "incubation",
|
||||
base_score: float = 50.0,
|
||||
decay_factor: float = 0.95,
|
||||
current_weight: float = None
|
||||
):
|
||||
self.id = id
|
||||
self.topic = topic
|
||||
self.related_etfs = related_etfs
|
||||
self.lifecycle_stage = lifecycle_stage # incubation | fermentation | realization | decay
|
||||
self.base_score = base_score
|
||||
self.decay_factor = decay_factor
|
||||
self.current_weight = current_weight if current_weight is not None else base_score
|
||||
self.last_updated = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
def decay(self) -> None:
|
||||
"""应用时间衰减"""
|
||||
self.current_weight *= self.decay_factor
|
||||
self.last_updated = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
def boost(self, new_score: float) -> None:
|
||||
"""新闻强化叙事权重"""
|
||||
# 使用加权平均,新信息占40%,历史占60%
|
||||
self.current_weight = self.current_weight * 0.6 + new_score * 0.4
|
||||
self.last_updated = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
def update_stage(self, new_stage: str) -> None:
|
||||
"""更新生命周期阶段"""
|
||||
valid_stages = ["incubation", "fermentation", "realization", "decay"]
|
||||
if new_stage in valid_stages:
|
||||
self.lifecycle_stage = new_stage
|
||||
self.last_updated = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典格式"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"topic": self.topic,
|
||||
"related_etfs": self.related_etfs,
|
||||
"lifecycle_stage": self.lifecycle_stage,
|
||||
"base_score": self.base_score,
|
||||
"decay_factor": self.decay_factor,
|
||||
"current_weight": round(self.current_weight, 2),
|
||||
"last_updated": self.last_updated
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Narrative':
|
||||
"""从字典创建对象"""
|
||||
return cls(
|
||||
id=data["id"],
|
||||
topic=data["topic"],
|
||||
related_etfs=data["related_etfs"],
|
||||
lifecycle_stage=data.get("lifecycle_stage", "incubation"),
|
||||
base_score=data.get("base_score", 50.0),
|
||||
decay_factor=data.get("decay_factor", 0.95),
|
||||
current_weight=data.get("current_weight")
|
||||
)
|
||||
|
||||
|
||||
class MacroCycle:
|
||||
"""宏观周期状态"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status: str = "neutral", # upward | downward | neutral
|
||||
liquidity: str = "neutral", # loose | neutral | tight
|
||||
policy_wind: str = "wait_and_see" # stimulus | regulation | wait_and_see
|
||||
):
|
||||
self.status = status
|
||||
self.liquidity = liquidity
|
||||
self.policy_wind = policy_wind
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {
|
||||
"status": self.status,
|
||||
"liquidity": self.liquidity,
|
||||
"policy_wind": self.policy_wind
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, str]) -> 'MacroCycle':
|
||||
return cls(
|
||||
status=data.get("status", "neutral"),
|
||||
liquidity=data.get("liquidity", "neutral"),
|
||||
policy_wind=data.get("policy_wind", "wait_and_see")
|
||||
)
|
||||
|
||||
|
||||
class WorldBook:
|
||||
"""
|
||||
MCP World Book - 系统核心数据中心
|
||||
======================================
|
||||
动态更新的状态管理器,存储:
|
||||
1. 宏观周期状态
|
||||
2. 活跃叙事对象列表
|
||||
3. 宏观因子影响矩阵快照
|
||||
"""
|
||||
|
||||
def __init__(self, data_dir: str = "data"):
|
||||
self.data_dir = Path(data_dir)
|
||||
self.data_dir.mkdir(exist_ok=True)
|
||||
|
||||
self.world_book_path = self.data_dir / "world_book.json"
|
||||
|
||||
# 初始化数据结构
|
||||
self.timestamp = datetime.now().isoformat()
|
||||
self.macro_cycle = MacroCycle()
|
||||
self.active_narratives: Dict[str, Narrative] = {}
|
||||
self.static_matrix_snapshot: Dict[str, Dict[str, float]] = {}
|
||||
|
||||
# 新增: 宏观因子向量 (用于向量点积计算)
|
||||
self.macro_factor_vector: Dict[str, float] = {}
|
||||
|
||||
# 尝试加载已有数据
|
||||
self.load()
|
||||
|
||||
def load(self) -> bool:
|
||||
"""从磁盘加载 World Book"""
|
||||
if not self.world_book_path.exists():
|
||||
print(f"[WorldBook] 未找到已有数据,初始化新的 World Book")
|
||||
self._load_static_matrix()
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(self.world_book_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.timestamp = data.get("timestamp", datetime.now().isoformat())
|
||||
self.macro_cycle = MacroCycle.from_dict(data.get("macro_cycle", {}))
|
||||
|
||||
# 加载叙事对象
|
||||
self.active_narratives = {}
|
||||
for narrative_data in data.get("active_narratives", []):
|
||||
narrative = Narrative.from_dict(narrative_data)
|
||||
self.active_narratives[narrative.id] = narrative
|
||||
|
||||
# 加载矩阵快照
|
||||
self.static_matrix_snapshot = data.get("static_matrix_snapshot", {})
|
||||
|
||||
# 加载宏观因子向量
|
||||
self.macro_factor_vector = data.get("macro_factor_vector", {})
|
||||
|
||||
print(f"[WorldBook] 成功加载数据,包含 {len(self.active_narratives)} 个活跃叙事")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[WorldBook] 加载数据失败: {e}")
|
||||
self._load_static_matrix()
|
||||
return False
|
||||
|
||||
def save(self) -> bool:
|
||||
"""保存 World Book 到磁盘"""
|
||||
try:
|
||||
self.timestamp = datetime.now().isoformat()
|
||||
|
||||
data = {
|
||||
"timestamp": self.timestamp,
|
||||
"macro_cycle": self.macro_cycle.to_dict(),
|
||||
"active_narratives": [
|
||||
narrative.to_dict()
|
||||
for narrative in self.active_narratives.values()
|
||||
],
|
||||
"static_matrix_snapshot": self.static_matrix_snapshot,
|
||||
"macro_factor_vector": self.macro_factor_vector
|
||||
}
|
||||
|
||||
with open(self.world_book_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"[WorldBook] 成功保存数据到 {self.world_book_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[WorldBook] 保存数据失败: {e}")
|
||||
return False
|
||||
|
||||
def _load_static_matrix(self) -> None:
|
||||
"""加载静态宏观矩阵"""
|
||||
matrix_path = Path("core") / "macro_matrix.json"
|
||||
if matrix_path.exists():
|
||||
try:
|
||||
with open(matrix_path, 'r', encoding='utf-8') as f:
|
||||
self.static_matrix_snapshot = json.load(f)
|
||||
print(f"[WorldBook] 加载宏观矩阵成功")
|
||||
except Exception as e:
|
||||
print(f"[WorldBook] 加载宏观矩阵失败: {e}")
|
||||
self.static_matrix_snapshot = {}
|
||||
|
||||
def add_narrative(self, narrative: Narrative) -> None:
|
||||
"""添加新叙事"""
|
||||
self.active_narratives[narrative.id] = narrative
|
||||
print(f"[WorldBook] 添加新叙事: {narrative.topic} (ID: {narrative.id})")
|
||||
|
||||
def update_narrative(self, narrative_id: str, **kwargs) -> bool:
|
||||
"""更新叙事属性"""
|
||||
if narrative_id not in self.active_narratives:
|
||||
print(f"[WorldBook] 叙事不存在: {narrative_id}")
|
||||
return False
|
||||
|
||||
narrative = self.active_narratives[narrative_id]
|
||||
|
||||
if 'new_score' in kwargs:
|
||||
narrative.boost(kwargs['new_score'])
|
||||
if 'stage' in kwargs:
|
||||
narrative.update_stage(kwargs['stage'])
|
||||
|
||||
return True
|
||||
|
||||
def decay_all_narratives(self) -> None:
|
||||
"""对所有叙事应用时间衰减"""
|
||||
for narrative in self.active_narratives.values():
|
||||
narrative.decay()
|
||||
print(f"[WorldBook] 对 {len(self.active_narratives)} 个叙事应用了时间衰减")
|
||||
|
||||
def remove_weak_narratives(self, threshold: float = 10.0) -> List[str]:
|
||||
"""移除权重过低的叙事"""
|
||||
to_remove = [
|
||||
nid for nid, narrative in self.active_narratives.items()
|
||||
if narrative.current_weight < threshold
|
||||
]
|
||||
|
||||
for nid in to_remove:
|
||||
topic = self.active_narratives[nid].topic
|
||||
del self.active_narratives[nid]
|
||||
print(f"[WorldBook] 移除衰退叙事: {topic} (ID: {nid})")
|
||||
|
||||
return to_remove
|
||||
|
||||
def get_narrative_by_topic(self, topic: str) -> Optional[Narrative]:
|
||||
"""根据主题获取叙事"""
|
||||
for narrative in self.active_narratives.values():
|
||||
if narrative.topic == topic:
|
||||
return narrative
|
||||
return None
|
||||
|
||||
def get_narratives_by_etf(self, etf_code: str) -> List[Narrative]:
|
||||
"""获取与特定 ETF 相关的所有叙事"""
|
||||
return [
|
||||
narrative for narrative in self.active_narratives.values()
|
||||
if etf_code in narrative.related_etfs
|
||||
]
|
||||
|
||||
def update_macro_cycle(self, **kwargs) -> None:
|
||||
"""更新宏观周期状态"""
|
||||
if 'status' in kwargs:
|
||||
self.macro_cycle.status = kwargs['status']
|
||||
if 'liquidity' in kwargs:
|
||||
self.macro_cycle.liquidity = kwargs['liquidity']
|
||||
if 'policy_wind' in kwargs:
|
||||
self.macro_cycle.policy_wind = kwargs['policy_wind']
|
||||
|
||||
print(f"[WorldBook] 宏观周期更新: {self.macro_cycle.to_dict()}")
|
||||
|
||||
def update_macro_factor_vector(self, factor_updates: Dict[str, float]) -> None:
|
||||
"""
|
||||
更新宏观因子向量
|
||||
|
||||
Args:
|
||||
factor_updates: 宏观因子及其强度,例如:
|
||||
{
|
||||
"interest_rate_down": 1.0, # 强烈降息预期
|
||||
"geopolitics_tension": 0.5, # 中等地缘紧张
|
||||
"policy_digital_economy": 1.0 # 数字经济政策头条
|
||||
}
|
||||
"""
|
||||
self.macro_factor_vector.update(factor_updates)
|
||||
print(f"[WorldBook] 宏观因子向量更新: {self.macro_factor_vector}")
|
||||
|
||||
def get_macro_factor_value(self, factor_name: str) -> float:
|
||||
"""获取特定宏观因子的当前值"""
|
||||
return self.macro_factor_vector.get(factor_name, 0.0)
|
||||
|
||||
def clear_macro_factor_vector(self) -> None:
|
||||
"""清空宏观因子向量(通常在每日开盘前重置)"""
|
||||
self.macro_factor_vector = {}
|
||||
print("[WorldBook] 宏观因子向量已清空")
|
||||
|
||||
def get_top_narratives(self, top_n: int = 5) -> List[Narrative]:
|
||||
"""获取权重最高的 N 个叙事"""
|
||||
sorted_narratives = sorted(
|
||||
self.active_narratives.values(),
|
||||
key=lambda n: n.current_weight,
|
||||
reverse=True
|
||||
)
|
||||
return sorted_narratives[:top_n]
|
||||
|
||||
def export_snapshot(self) -> Dict[str, Any]:
|
||||
"""导出完整快照(供其他 Agent 读取)"""
|
||||
return {
|
||||
"timestamp": self.timestamp,
|
||||
"macro_cycle": self.macro_cycle.to_dict(),
|
||||
"macro_factor_vector": self.macro_factor_vector,
|
||||
"active_narratives": [n.to_dict() for n in self.active_narratives.values()],
|
||||
"static_matrix_snapshot": self.static_matrix_snapshot,
|
||||
"summary": {
|
||||
"total_narratives": len(self.active_narratives),
|
||||
"top_3_topics": [n.topic for n in self.get_top_narratives(3)]
|
||||
}
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<WorldBook: {len(self.active_narratives)} narratives, "
|
||||
f"macro={self.macro_cycle.status}, "
|
||||
f"updated={self.timestamp}>"
|
||||
)
|
||||
|
||||
|
||||
# ==================== 工具函数 ====================
|
||||
|
||||
def create_narrative_id(topic: str, date: Optional[str] = None) -> str:
|
||||
"""生成叙事 ID"""
|
||||
if date is None:
|
||||
date = datetime.now().strftime("%Y%m")
|
||||
|
||||
# 简化主题名称作为 ID 的一部分
|
||||
topic_slug = topic.replace(" ", "_").replace(",", "_").replace("、", "_")
|
||||
return f"narrative_{topic_slug}_{date}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("=" * 50)
|
||||
print("World Book 核心模块测试")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建 World Book 实例
|
||||
wb = WorldBook(data_dir="data")
|
||||
|
||||
# 添加测试叙事
|
||||
narrative1 = Narrative(
|
||||
id=create_narrative_id("低空经济"),
|
||||
topic="低空经济",
|
||||
related_etfs=["512980", "159969"],
|
||||
lifecycle_stage="fermentation",
|
||||
base_score=85.0,
|
||||
decay_factor=0.95
|
||||
)
|
||||
|
||||
narrative2 = Narrative(
|
||||
id=create_narrative_id("AI算力"),
|
||||
topic="AI算力",
|
||||
related_etfs=["515980", "159813"],
|
||||
lifecycle_stage="realization",
|
||||
base_score=92.0,
|
||||
decay_factor=0.93
|
||||
)
|
||||
|
||||
wb.add_narrative(narrative1)
|
||||
wb.add_narrative(narrative2)
|
||||
|
||||
# 更新宏观周期
|
||||
wb.update_macro_cycle(
|
||||
status="upward",
|
||||
liquidity="loose",
|
||||
policy_wind="stimulus"
|
||||
)
|
||||
|
||||
# 保存数据
|
||||
wb.save()
|
||||
|
||||
# 测试导出快照
|
||||
snapshot = wb.export_snapshot()
|
||||
print("\n当前状态快照:")
|
||||
print(json.dumps(snapshot, ensure_ascii=False, indent=2))
|
||||
|
||||
# 测试衰减
|
||||
print("\n应用时间衰减...")
|
||||
wb.decay_all_narratives()
|
||||
|
||||
# 测试获取 Top 叙事
|
||||
top = wb.get_top_narratives(2)
|
||||
print(f"\nTop 2 叙事:")
|
||||
for n in top:
|
||||
print(f" - {n.topic}: {n.current_weight:.2f}")
|
||||
|
||||
print("\n✅ World Book 核心模块测试完成")
|
||||
303
docs/crawler_guide.md
Normal file
303
docs/crawler_guide.md
Normal file
@ -0,0 +1,303 @@
|
||||
# Skill F: 新闻联播爬虫使用指南
|
||||
|
||||
## 功能说明
|
||||
|
||||
Skill F (Crawler) 使用 **cloudscraper** + **BeautifulSoup** 自动抓取新闻联播官方网站的文字稿,并转换为符合 Skill A (Analyst) 要求的格式。
|
||||
|
||||
## 技术特点
|
||||
|
||||
- **绕过防护**: 使用 cloudscraper 自动处理 Cloudflare 等反爬机制
|
||||
- **稳定可靠**: 同步爬取,无需处理异步复杂性
|
||||
- **精准解析**: 基于实际网站 DOM 结构(.content-section)
|
||||
|
||||
## 数据源
|
||||
|
||||
- **网站**: https://cn.govopendata.com/xinwenlianbo/
|
||||
- **更新频率**: 每日 19:00 后更新
|
||||
- **数据格式**: 新闻标题 + 正文内容
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
pip install cloudscraper beautifulsoup4
|
||||
```
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 1. 命令行测试
|
||||
|
||||
```bash
|
||||
# 测试爬虫功能
|
||||
python skills/crawler.py
|
||||
```
|
||||
|
||||
### 2. Python 代码调用
|
||||
|
||||
```python
|
||||
from skills.crawler import XinwenLianboCrawler
|
||||
|
||||
# 创建爬虫实例
|
||||
crawler = XinwenLianboCrawler()
|
||||
|
||||
# 爬取今日新闻
|
||||
news_list = crawler.crawl_sync()
|
||||
|
||||
# 爬取指定日期
|
||||
news_list = crawler.crawl_sync(date="20251229")
|
||||
|
||||
# 输出格式:
|
||||
# [
|
||||
# {"title": "新闻标题1", "text": "新闻正文1..."},
|
||||
# {"title": "新闻标题2", "text": "新闻正文2..."},
|
||||
# ...
|
||||
# ]
|
||||
```
|
||||
|
||||
### 3. 批量爬取
|
||||
|
||||
```python
|
||||
from skills.crawler import XinwenLianboCrawler
|
||||
|
||||
crawler = XinwenLianboCrawler()
|
||||
|
||||
# 爬取一段时间范围的新闻
|
||||
results = crawler.crawl_date_range(
|
||||
start_date="20251220",
|
||||
end_date="20251229"
|
||||
)
|
||||
|
||||
# results 是一个字典: {date: news_list}
|
||||
for date, news_list in results.items():
|
||||
print(f"{date}: {len(news_list)} 条新闻")
|
||||
```
|
||||
|
||||
## 与 Skill A 集成
|
||||
|
||||
```python
|
||||
from skills.crawler import XinwenLianboCrawler
|
||||
from skills.analyst import NewsAnalyzer
|
||||
|
||||
# 1. 爬取新闻
|
||||
crawler = XinwenLianboCrawler()
|
||||
news_list = crawler.crawl_sync()
|
||||
|
||||
# 2. 分析新闻
|
||||
analyzer = NewsAnalyzer()
|
||||
analysis_results = analyzer.batch_analyze(news_list)
|
||||
|
||||
# 3. 生成叙事 JSON
|
||||
narrative_json = analyzer.generate_narrative_json(analysis_results)
|
||||
|
||||
print(f"识别到 {len(narrative_json['narratives'])} 个投资叙事")
|
||||
```
|
||||
|
||||
## 在主流程中使用
|
||||
|
||||
```bash
|
||||
# 方式1: 使用爬虫(默认)
|
||||
python main.py --mode daily
|
||||
|
||||
# 方式2: 不使用爬虫(使用测试数据)
|
||||
python main.py --mode daily --no-crawler
|
||||
```
|
||||
|
||||
## 数据缓存
|
||||
|
||||
爬虫会自动缓存抓取的新闻到 `data/news_cache/` 目录:
|
||||
|
||||
```
|
||||
data/news_cache/
|
||||
├── xinwenlianbo_20251229.json
|
||||
├── xinwenlianbo_20251230.json
|
||||
└── ...
|
||||
```
|
||||
|
||||
当网络失败时,系统会自动尝试从缓存读取。
|
||||
|
||||
## 批量爬取历史数据
|
||||
|
||||
```python
|
||||
from skills.crawler import XinwenLianboCrawler
|
||||
|
||||
crawler = XinwenLianboCrawler()
|
||||
|
||||
# 爬取一段时间范围的新闻
|
||||
results = crawler.crawl_date_range(
|
||||
start_date="20251220",
|
||||
end_date="20251229"
|
||||
)
|
||||
|
||||
# results 是一个字典: {date: news_list}
|
||||
for date, news_list in results.items():
|
||||
print(f"{date}: {len(news_list)} 条新闻")
|
||||
```
|
||||
|
||||
## 自定义解析逻辑
|
||||
|
||||
如果网站结构发生变化,可以修改 `_parse_content()` 方法:
|
||||
|
||||
```python
|
||||
def _parse_content(self, markdown: str, html: str) -> List[Dict[str, str]]:
|
||||
"""
|
||||
自定义解析逻辑
|
||||
|
||||
Args:
|
||||
markdown: Crawl4AI 提取的 Markdown
|
||||
html: 原始 HTML
|
||||
|
||||
Returns:
|
||||
[{"title": "", "text": ""}]
|
||||
"""
|
||||
# 在这里添加你的解析逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1: 爬虫返回空列表
|
||||
|
||||
**可能原因**:
|
||||
- 网站结构变化
|
||||
- 当天新闻尚未更新(新闻联播通常 19:30 播出,网站 20:00 后更新)
|
||||
- 网络连接问题
|
||||
- 被 Cloudflare 拦截(虽然使用了 cloudscraper,但仍可能发生)
|
||||
|
||||
**解决方法**:
|
||||
```bash
|
||||
# 检查爬虫输出
|
||||
python skills/crawler.py
|
||||
|
||||
# 查看缓存文件
|
||||
cat data/news_cache/xinwenlianbo_*.json
|
||||
|
||||
# 检查网站是否可访问
|
||||
curl https://cn.govopendata.com/xinwenlianbo/20251229/
|
||||
```
|
||||
|
||||
### 问题2: cloudscraper 安装失败
|
||||
|
||||
**解决方法**:
|
||||
```bash
|
||||
# 升级 pip
|
||||
pip install --upgrade pip
|
||||
|
||||
# 单独安装 cloudscraper
|
||||
pip install cloudscraper --upgrade
|
||||
|
||||
# 如果仍然失败,安装依赖
|
||||
pip install requests[security] pyparsing
|
||||
```
|
||||
|
||||
### 问题3: 解析结果不准确
|
||||
|
||||
**解决方法**:
|
||||
编辑 `skills/crawler.py`,检查 `_parse_content()` 方法中的 CSS 选择器是否与网站实际结构匹配:
|
||||
|
||||
```python
|
||||
# 当前使用的选择器
|
||||
sections = soup.select('.content-section')
|
||||
title_tag = section.select_one('.content-heading')
|
||||
content_tag = section.select_one('.content-body')
|
||||
```
|
||||
|
||||
可以手动访问网站,使用浏览器开发者工具(F12)查看实际的 DOM 结构。
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 使用缓存
|
||||
|
||||
爬虫会自动缓存结果,避免重复请求:
|
||||
|
||||
```python
|
||||
# 首次爬取(从网站)
|
||||
news_list = crawler.crawl_sync(date="20251229")
|
||||
|
||||
# 再次爬取(从缓存,如果网络失败)
|
||||
news_list = crawler.crawl_sync(date="20251229") # 自动读取缓存
|
||||
```
|
||||
|
||||
### 2. 批量爬取时添加延迟
|
||||
|
||||
```python
|
||||
crawler = XinwenLianboCrawler()
|
||||
|
||||
# crawl_date_range 已内置 2 秒延迟
|
||||
results = crawler.crawl_date_range("20251220", "20251229")
|
||||
```
|
||||
|
||||
## 定时任务配置
|
||||
|
||||
### Linux/Mac (crontab)
|
||||
|
||||
```bash
|
||||
# 编辑 crontab
|
||||
crontab -e
|
||||
|
||||
# 添加定时任务(每天 20:30 执行)
|
||||
30 20 * * * cd /path/to/MND-IA && python skills/crawler.py >> logs/crawler.log 2>&1
|
||||
```
|
||||
|
||||
### Windows (任务计划程序)
|
||||
|
||||
1. 打开"任务计划程序"
|
||||
2. 创建基本任务
|
||||
3. 触发器:每天 20:30
|
||||
4. 操作:启动程序 `python.exe`
|
||||
5. 参数:`E:\02projects\MND-IA\skills\crawler.py`
|
||||
|
||||
## API 参考
|
||||
|
||||
### XinwenLianboCrawler
|
||||
|
||||
#### `__init__(base_url)`
|
||||
- 初始化爬虫
|
||||
- `base_url`: 新闻联播网站基础 URL
|
||||
- 自动创建 cloudscraper 会话
|
||||
|
||||
#### `crawl_sync(date=None)`
|
||||
- 同步爬取接口(推荐使用)
|
||||
- `date`: 日期字符串 (YYYYMMDD),默认今天
|
||||
- 返回: `List[Dict[str, str]]`
|
||||
|
||||
#### `crawl_xinwenlianbo(date=None)`
|
||||
- 爬取核心方法
|
||||
- `date`: 日期字符串
|
||||
- 返回: `List[Dict[str, str]]`
|
||||
|
||||
#### `crawl_date_range(start_date, end_date)`
|
||||
- 批量爬取日期范围
|
||||
- `start_date`: 开始日期 (YYYYMMDD)
|
||||
- `end_date`: 结束日期 (YYYYMMDD)
|
||||
- 返回: `Dict[str, List[Dict]]`
|
||||
- 自动添加 2 秒延迟避免请求过快
|
||||
|
||||
## 扩展建议
|
||||
|
||||
### 1. 添加更多新闻源
|
||||
|
||||
可以参考 Skill F 的实现,添加其他新闻源爬虫:
|
||||
|
||||
- 财联社快讯
|
||||
- 人民日报
|
||||
- 发改委官网
|
||||
- 各部委新闻发布
|
||||
|
||||
### 2. 使用 LLM 增强提取
|
||||
|
||||
```python
|
||||
# 配置 LLM 客户端
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(api_key="your-api-key")
|
||||
|
||||
# 使用 LLM 辅助提取(需要实现相关代码)
|
||||
news_list = await crawler.crawl_xinwenlianbo(use_llm=True)
|
||||
```
|
||||
|
||||
### 3. 集成到实时监控
|
||||
|
||||
监听新闻联播更新,实时触发分析流程。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
417
main.py
Normal file
417
main.py
Normal file
@ -0,0 +1,417 @@
|
||||
"""
|
||||
MND-IA Main Orchestrator
|
||||
=========================
|
||||
主调度程序 - 协调所有 Agent Skills 完成每日投资决策流程
|
||||
|
||||
执行流程:
|
||||
1. Ingest (数据摄取): Analyst 读取新闻, Quant 读取行情
|
||||
2. Cognition (认知更新): Strategist 更新 WorldBook
|
||||
3. Decision (决策生成): PM 计算 Trust Index 并生成订单
|
||||
4. Execution (执行输出): 输出交易指令
|
||||
5. Audit (复盘审计): Auditor 评估昨日预测准确性
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import json
|
||||
import argparse
|
||||
|
||||
# 导入核心模块
|
||||
from core.world_book import WorldBook
|
||||
|
||||
# 导入 Agent Skills
|
||||
from skills.analyst import NewsAnalyzer
|
||||
from skills.quant import QuantAnalyzer, get_all_etf_codes_from_asset_map
|
||||
from skills.strategist import MacroStrategist
|
||||
from skills.pm import PortfolioManager, save_orders_to_file
|
||||
from skills.auditor import PerformanceAuditor
|
||||
from skills.crawler import XinwenLianboCrawler
|
||||
|
||||
|
||||
class MNDAgent:
|
||||
"""MND-IA 主智能体"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_dir: str = "data",
|
||||
total_capital: float = 1000000.0,
|
||||
llm_client=None,
|
||||
use_llm: bool = True
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
data_dir: 数据目录
|
||||
total_capital: 总资金(元)
|
||||
llm_client: LLM 客户端(可选,用于 Analyst 增强)
|
||||
use_llm: 是否使用 LLM 进行智能分析(默认 True)
|
||||
"""
|
||||
self.data_dir = Path(data_dir)
|
||||
self.data_dir.mkdir(exist_ok=True)
|
||||
self.use_llm = use_llm
|
||||
|
||||
# 初始化核心
|
||||
print("=" * 60)
|
||||
print("🚀 MND-IA 系统启动")
|
||||
print(f" 模式: {'LLM 智能分析' if use_llm else '规则引擎(降级模式)'}")
|
||||
print("=" * 60)
|
||||
|
||||
self.world_book = WorldBook(data_dir=data_dir)
|
||||
|
||||
# 初始化 Agent Skills(传递 use_llm 参数)
|
||||
self.analyst = NewsAnalyzer(llm_client=llm_client, use_llm=use_llm)
|
||||
self.quant = QuantAnalyzer()
|
||||
self.strategist = MacroStrategist(self.world_book, use_llm=use_llm)
|
||||
self.pm = PortfolioManager(self.world_book, total_capital=total_capital, use_llm=use_llm)
|
||||
self.auditor = PerformanceAuditor(data_dir=data_dir)
|
||||
self.crawler = XinwenLianboCrawler()
|
||||
|
||||
print("✅ 所有 Agent Skills 初始化完成\n")
|
||||
|
||||
def daily_batch_workflow(
|
||||
self,
|
||||
news_list: list = None,
|
||||
etf_codes: list = None,
|
||||
use_crawler: bool = True
|
||||
) -> dict:
|
||||
"""
|
||||
每日批处理流程(08:30 AM 执行)
|
||||
|
||||
Args:
|
||||
news_list: 新闻列表(如为 None 则使用爬虫或测试数据)
|
||||
etf_codes: ETF 代码列表(如为 None 则使用全部)
|
||||
use_crawler: 是否使用爬虫抓取新闻联播(默认 True)
|
||||
|
||||
Returns:
|
||||
执行结果汇总
|
||||
"""
|
||||
print(f"📅 开始执行每日批处理流程")
|
||||
print(f"⏰ 执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 60)
|
||||
|
||||
# ========== Phase 1: 数据摄取 (Ingest) ==========
|
||||
print("\n【Phase 1: 数据摄取】")
|
||||
print("-" * 60)
|
||||
|
||||
# 1.1 获取新闻数据
|
||||
if news_list is None:
|
||||
if use_crawler:
|
||||
print("🕷️ 使用爬虫抓取新闻联播...")
|
||||
news_list = self.crawler.crawl_sync()
|
||||
|
||||
if not news_list:
|
||||
print("⚠️ 爬虫未获取到数据,使用测试数据")
|
||||
news_list = self._get_test_news()
|
||||
else:
|
||||
print("⚠️ 未提供新闻数据,使用测试数据")
|
||||
news_list = self._get_test_news()
|
||||
|
||||
print(f"📰 新闻分析: {len(news_list)} 条")
|
||||
news_analysis = self.analyst.batch_analyze(news_list)
|
||||
narrative_json = self.analyst.generate_narrative_json(news_analysis)
|
||||
|
||||
# 1.2 获取行情数据
|
||||
if etf_codes is None:
|
||||
etf_codes = get_all_etf_codes_from_asset_map()
|
||||
print(f"📊 行情获取: {len(etf_codes)} 个 ETF")
|
||||
|
||||
market_data = self.quant.batch_analyze(etf_codes)
|
||||
market_report = self.quant.generate_market_report(market_data)
|
||||
|
||||
# ========== Phase 2: 认知更新 (Cognition) ==========
|
||||
print("\n【Phase 2: 认知更新】")
|
||||
print("-" * 60)
|
||||
|
||||
# 2.1 处理叙事 JSON
|
||||
self.strategist.process_narrative_json(narrative_json)
|
||||
|
||||
# 2.2 检测宏观周期变化
|
||||
cycle_change = self.strategist.detect_macro_cycle_change(
|
||||
narrative_json,
|
||||
market_report
|
||||
)
|
||||
|
||||
if cycle_change:
|
||||
self.strategist.apply_macro_cycle_change(cycle_change)
|
||||
|
||||
# 2.3 每日维护
|
||||
maintenance_report = self.strategist.daily_maintenance()
|
||||
|
||||
# 2.4 生成 WorldBook 快照
|
||||
wb_snapshot = self.strategist.generate_world_book_snapshot()
|
||||
|
||||
# ========== Phase 3: 决策生成 (Decision) ==========
|
||||
print("\n【Phase 3: 决策生成】")
|
||||
print("-" * 60)
|
||||
|
||||
# 3.1 计算 Trust Index
|
||||
trust_results = self.pm.batch_calculate_trust_index(market_data)
|
||||
|
||||
# 3.2 生成交易订单
|
||||
raw_orders = self.pm.generate_trade_orders(trust_results, market_data)
|
||||
|
||||
# 3.3 风控检查
|
||||
final_orders = self.pm.apply_risk_control(raw_orders)
|
||||
|
||||
# 3.4 生成投资组合报告
|
||||
portfolio_report = self.pm.generate_portfolio_report(
|
||||
trust_results,
|
||||
final_orders
|
||||
)
|
||||
|
||||
# ========== Phase 4: 执行输出 (Execution) ==========
|
||||
print("\n【Phase 4: 执行输出】")
|
||||
print("-" * 60)
|
||||
|
||||
if final_orders:
|
||||
# 保存订单到文件
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
save_orders_to_file(final_orders, f"orders_{timestamp}.json")
|
||||
|
||||
print(f"📋 生成 {len(final_orders)} 条交易指令:")
|
||||
for order in final_orders[:5]: # 显示前5条
|
||||
print(f" {order['action'].upper()} {order['code']}: "
|
||||
f"{order['shares']}股 @ ¥{order['price']}")
|
||||
else:
|
||||
print("⚠️ 无符合条件的交易机会")
|
||||
|
||||
# 保存 WorldBook
|
||||
self.world_book.save()
|
||||
|
||||
# ========== Phase 5: 结果汇总 ==========
|
||||
execution_result = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"workflow": "daily_batch",
|
||||
"phases": {
|
||||
"ingest": {
|
||||
"news_count": len(news_list),
|
||||
"etf_count": len(etf_codes),
|
||||
"narratives_identified": len(narrative_json['narratives'])
|
||||
},
|
||||
"cognition": {
|
||||
"active_narratives": len(self.world_book.active_narratives),
|
||||
"macro_cycle": self.world_book.macro_cycle.to_dict(),
|
||||
"cycle_changed": cycle_change is not None
|
||||
},
|
||||
"decision": {
|
||||
"trust_results": len(trust_results),
|
||||
"orders_generated": len(final_orders),
|
||||
"capital_utilization": portfolio_report['risk_control']['capital_utilization']
|
||||
}
|
||||
},
|
||||
"top_opportunities": portfolio_report['top_opportunities'][:3],
|
||||
"orders": final_orders
|
||||
}
|
||||
|
||||
# 保存执行记录
|
||||
self._save_execution_log(execution_result)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ 每日批处理流程完成")
|
||||
print("=" * 60)
|
||||
|
||||
return execution_result
|
||||
|
||||
def post_market_audit(
|
||||
self,
|
||||
yesterday_predictions: list = None
|
||||
) -> dict:
|
||||
"""
|
||||
盘后复盘流程(16:00 PM 执行)
|
||||
|
||||
Args:
|
||||
yesterday_predictions: 昨日的预测记录
|
||||
|
||||
Returns:
|
||||
审计报告
|
||||
"""
|
||||
print("\n📊 开始盘后复盘审计")
|
||||
print("=" * 60)
|
||||
|
||||
if yesterday_predictions is None:
|
||||
print("⚠️ 无昨日预测数据,跳过审计")
|
||||
return {"status": "skipped", "reason": "no_predictions"}
|
||||
|
||||
# 获取实际表现数据(这里需要从行情数据计算)
|
||||
actual_performances = self._fetch_actual_performances(yesterday_predictions)
|
||||
|
||||
# 执行审计
|
||||
audit_summary = self.auditor.batch_audit(
|
||||
yesterday_predictions,
|
||||
actual_performances
|
||||
)
|
||||
|
||||
# 生成报告
|
||||
report = self.auditor.generate_correction_report(audit_summary)
|
||||
|
||||
# 保存报告
|
||||
report_path = self.data_dir / f"audit_report_{datetime.now().strftime('%Y%m%d')}.md"
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
|
||||
print(f"\n📄 审计报告已保存: {report_path}")
|
||||
print(f"准确率: {audit_summary['performance']['accuracy']:.1f}%")
|
||||
|
||||
return audit_summary
|
||||
|
||||
def intraday_sentinel(
|
||||
self,
|
||||
breaking_news: dict
|
||||
) -> dict:
|
||||
"""
|
||||
盘中哨兵流程(09:30 - 15:00)
|
||||
|
||||
当检测到 Level A 重大新闻时触发
|
||||
|
||||
Args:
|
||||
breaking_news: 快讯内容
|
||||
|
||||
Returns:
|
||||
应急响应结果
|
||||
"""
|
||||
print("\n🚨 盘中哨兵触发")
|
||||
print("=" * 60)
|
||||
|
||||
# 分析快讯
|
||||
analysis = self.analyst.analyze_news(
|
||||
breaking_news.get('text', ''),
|
||||
breaking_news.get('title', '')
|
||||
)
|
||||
|
||||
# 如果不是 Level A,忽略
|
||||
if analysis['level'] != 'A':
|
||||
print("⚠️ 非 A 级新闻,不触发应急响应")
|
||||
return {"triggered": False, "reason": "not_level_a"}
|
||||
|
||||
print(f"⚠️ 检测到 Level A 重大新闻: {analysis['title']}")
|
||||
print(f" 涉及板块: {', '.join(analysis['sectors'])}")
|
||||
|
||||
# 立即更新 WorldBook
|
||||
narrative_json = self.analyst.generate_narrative_json([analysis])
|
||||
self.strategist.process_narrative_json(narrative_json)
|
||||
|
||||
# 检查宏观周期变化
|
||||
cycle_change = self.strategist.detect_macro_cycle_change(narrative_json)
|
||||
if cycle_change:
|
||||
self.strategist.apply_macro_cycle_change(cycle_change)
|
||||
|
||||
# 重新评估持仓(如果有)
|
||||
# 这里简化处理,实际应该重新计算 Trust Index
|
||||
|
||||
self.world_book.save()
|
||||
|
||||
return {
|
||||
"triggered": True,
|
||||
"news_level": "A",
|
||||
"sectors_affected": analysis['sectors'],
|
||||
"macro_cycle_changed": cycle_change is not None,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def _get_test_news(self) -> list:
|
||||
"""获取测试新闻数据"""
|
||||
return [
|
||||
{
|
||||
"title": "工信部发布低空经济指导意见 2027年规模达1万亿",
|
||||
"text": "工业和信息化部发布《低空经济高质量发展指导意见》,推动 eVTOL 产业化。"
|
||||
},
|
||||
{
|
||||
"title": "AI算力需求持续旺盛 多家公司业绩预增50%",
|
||||
"text": "智算中心建设加速,算力租赁业务快速增长。"
|
||||
}
|
||||
]
|
||||
|
||||
def _fetch_actual_performances(
|
||||
self,
|
||||
predictions: list
|
||||
) -> dict:
|
||||
"""获取实际表现数据(简化版)"""
|
||||
# 实际应该从 Quant 模块获取真实数据
|
||||
# 这里返回模拟数据
|
||||
performances = {}
|
||||
for pred in predictions:
|
||||
code = pred['code']
|
||||
performances[code] = {
|
||||
"code": code,
|
||||
"price_change": 0, # 需要实际计算
|
||||
"volume_change": 1.0,
|
||||
"date": datetime.now().strftime("%Y-%m-%d")
|
||||
}
|
||||
return performances
|
||||
|
||||
def _save_execution_log(self, result: dict) -> None:
|
||||
"""保存执行日志"""
|
||||
log_path = self.data_dir / "execution_logs.json"
|
||||
|
||||
logs = []
|
||||
if log_path.exists():
|
||||
with open(log_path, 'r', encoding='utf-8') as f:
|
||||
logs = json.load(f)
|
||||
|
||||
logs.append(result)
|
||||
|
||||
# 只保留最近30天
|
||||
logs = logs[-30:]
|
||||
|
||||
with open(log_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(logs, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
"""命令行入口"""
|
||||
parser = argparse.ArgumentParser(description="MND-IA 投资智能体")
|
||||
parser.add_argument(
|
||||
'--mode',
|
||||
choices=['daily', 'audit', 'test'],
|
||||
default='test',
|
||||
help='运行模式: daily(每日批处理), audit(盘后审计), test(测试)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--capital',
|
||||
type=float,
|
||||
default=1000000.0,
|
||||
help='总资金(元)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-crawler',
|
||||
action='store_true',
|
||||
help='不使用爬虫,使用测试数据'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-llm',
|
||||
action='store_true',
|
||||
help='不使用 LLM,使用规则引擎(降级模式)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建 Agent 实例
|
||||
agent = MNDAgent(
|
||||
total_capital=args.capital,
|
||||
use_llm=not args.no_llm
|
||||
)
|
||||
|
||||
if args.mode == 'daily':
|
||||
# 每日批处理
|
||||
result = agent.daily_batch_workflow(use_crawler=not args.no_crawler)
|
||||
print(f"\n执行结果:")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
elif args.mode == 'audit':
|
||||
# 盘后审计(需要提供昨日数据)
|
||||
print("盘后审计模式需要提供昨日预测数据")
|
||||
|
||||
else:
|
||||
# 测试模式
|
||||
print("\n🧪 测试模式:执行完整流程演示\n")
|
||||
result = agent.daily_batch_workflow(use_crawler=not args.no_crawler)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("📈 Top 投资机会:")
|
||||
print("=" * 60)
|
||||
for opp in result['top_opportunities']:
|
||||
print(f" {opp['code']}: Trust Index {opp['trust_index']:.1f} - {opp['verdict']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
153
readme.md
Normal file
153
readme.md
Normal file
@ -0,0 +1,153 @@
|
||||
|
||||
# Macro-Narrative Driven ETF Investment Agent (MND-IA)
|
||||
|
||||
## 1. 项目概述 (Project Overview)
|
||||
|
||||
**MND-IA** 是一个基于 **“宏观叙事生命周期 + 资金流向交叉验证”** 的自动化 ETF 投资智能体系统。
|
||||
|
||||
### 核心哲学 (Core Philosophy)
|
||||
|
||||
1. **叙事即信号 (Narrative as Signal):** A 股是政策市,新闻联播及部委政策是市场趋势的先行指标。
|
||||
2. **资金即真相 (Flow as Truth):** 只有资金实际流入的板块,叙事才具备可信度。
|
||||
3. **信任指数 (Trust Index):** 决策基于叙事热度与资金强度的共振程度。
|
||||
4. **生命周期管理 (Lifecycle):** 新闻不是孤立的脉冲,而是具有“潜伏-发酵-兑现-衰退”周期的持久对象。
|
||||
|
||||
---
|
||||
|
||||
## 2. 系统架构 (System Architecture)
|
||||
|
||||
本系统采用 **Multi-Agent "Skills" 架构**,由一个不可拆分的**共享内核 (Shared Kernel)** 和五个独立职能的 **Agent Skills** 组成。
|
||||
|
||||
### 2.1 共享内核 (Shared Kernel) - *不可拆分*
|
||||
|
||||
这是系统的数据中心和逻辑基石,所有 Agent 必须遵循此协议。
|
||||
|
||||
1. **MCP World Book (世界书):** 一个动态更新的 JSON 数据库,存储宏观状态、活跃叙事对象和历史经验。
|
||||
2. **Asset Map (资产映射表):** 连接自然语言(如“低空经济”)与金融代码(如 `512XXX`)的静态字典。
|
||||
3. **Macro Matrix (宏观逻辑矩阵):** 定义宏观因子(如利率、汇率)对各 ETF 板块的**基础理论影响值 ()**。
|
||||
|
||||
### 2.2 Agent Skills 群 (The Squad)
|
||||
|
||||
| Agent ID | 名称 | 职能 (Skill) | 输入 | 输出 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **Skill_A** | **情报分析师** (Analyst) | NLP 提取、新闻分级、叙事打分 | 新闻文本 (News) | `Narrative_JSON` |
|
||||
| **Skill_B** | **量化研究员** (Quant) | 行情分析、资金流计算、异常检测 | 行情数据 (Akshare) | `Market_Data_JSON` |
|
||||
| **Skill_C** | **宏观策略师** (Strategist) | 维护世界书、判断周期、更新叙事权重 | `Narrative_JSON` + 历史 WorldBook | 更新后的 `World_Book` |
|
||||
| **Skill_D** | **基金经理** (PM) | 计算信任指数、风控、生成订单 | `World_Book` + `Market_Data_JSON` | `Trade_Orders` |
|
||||
| **Skill_E** | **复盘审计员** (Auditor) | 绩效归因、偏差修正 | 历史预测 vs 实际走势 | `Correction_Report` |
|
||||
| **Skill_F** | **新闻联播爬虫** (Crawler) | 抓取新闻联播文字稿 | 目标日期 | 新闻文本列表 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据协议与 Schema (Data Protocols)
|
||||
|
||||
**注意:AI 生成代码时必须严格遵守以下 JSON 结构。**
|
||||
|
||||
### 3.1 World Book Schema (世界书结构)
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-10-01T09:00:00",
|
||||
"macro_cycle": {
|
||||
"status": "upward", // upward(上行) | downward(下行)
|
||||
"liquidity": "loose", // loose | neutral | tight
|
||||
"policy_wind": "stimulus" // stimulus | regulation | wait_and_see
|
||||
},
|
||||
"active_narratives": [
|
||||
{
|
||||
"id": "narrative_low_altitude_economy_202510",
|
||||
"topic": "低空经济",
|
||||
"related_etfs": ["512XXX", "159XXX"],
|
||||
"lifecycle_stage": "fermentation", // incubation | fermentation | realization | decay
|
||||
"base_score": 85, // 初始新闻强度
|
||||
"decay_factor": 0.95, // 每日衰减系数
|
||||
"current_weight": 80.75, // 动态权重
|
||||
"last_updated": "2025-10-01"
|
||||
}
|
||||
],
|
||||
"static_matrix_snapshot": {
|
||||
// 基础宏观因子影响表 ($x$)
|
||||
"tech_etf": {"rate_cut": 8, "currency_depreciation": 2},
|
||||
"bank_etf": {"rate_cut": -5, "currency_depreciation": -1}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 3.2 Trust Index Formula (信任指数算法)
|
||||
|
||||
Agent D (PM) 必须使用以下逻辑计算得分:
|
||||
|
||||
* **Score_narrative:** 来自 World Book 中 `current_weight`。
|
||||
* **Score_flow:** 来自 Skill_B (Quant) 的资金净流入归一化评分。
|
||||
* **Penalty_divergence:** 当 `Narrative > 80` 但 `Flow < 30` 时,惩罚值设为 100 (一票否决)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 业务流程 (Workflows)
|
||||
|
||||
### 4.1 每日批处理流程 (Daily Batch) - *08:30 AM*
|
||||
|
||||
1. **Ingest:** Skill_A 读取昨夜今晨新闻,Skill_B 读取昨日行情。
|
||||
2. **Cognition:** Skill_C 读取 Skill_A 的结果,更新 MCP World Book(增强或衰减叙事权重)。
|
||||
3. **Decision:** Skill_D 读取最新的 World Book 和 Skill_B 的资金数据,计算 Trust Index。
|
||||
4. **Execution:** Skill_D 输出具体的 `buy/sell` 指令列表。
|
||||
|
||||
### 4.2 盘中哨兵流程 (Intraday Sentinel) - *09:30 - 15:00*
|
||||
|
||||
1. **Monitor:** 系统监听快讯接口(如财联社红色资讯)。
|
||||
2. **Trigger:** 若检测到 `Level A` 级新闻(如印花税、降准)。
|
||||
3. **Interrupt:** 唤醒 Skill_C 临时修改 World Book 状态。
|
||||
4. **Re-eval:** 触发 Skill_D 重新计算当日持仓建议。
|
||||
|
||||
### 4.3 盘后复盘流程 (Post-Market Feedback) - *16:00 PM*
|
||||
|
||||
1. Skill_E 对比昨日的 `Trade_Orders` 和今日实际涨跌幅。
|
||||
2. 若偏差过大,Skill_E 向 MCP 写入一条 `correction_log`,调整 Skill_C 未来对同类新闻的打分权重。
|
||||
|
||||
---
|
||||
|
||||
## 5. 项目结构 (Project Structure)
|
||||
|
||||
建议 AI 按照以下目录结构生成代码:
|
||||
|
||||
```text
|
||||
/mnd_agent
|
||||
├── /core # 共享内核
|
||||
│ ├── world_book.py # MCP 读写逻辑
|
||||
│ ├── asset_map.json # 资产映射字典
|
||||
│ └── macro_matrix.json # 静态因子表 ($x$)
|
||||
├── /skills # Agent 能力群
|
||||
│ ├── analyst.py # Skill A: LLM Prompts for News
|
||||
│ ├── quant.py # Skill B: Akshare Data Analysis
|
||||
│ ├── strategist.py # Skill C: World Book Update Logic
|
||||
│ ├── pm.py # Skill D: Trust Index & Trade Logic
|
||||
│ ├── auditor.py # Skill E: Feedback Loop
|
||||
│ └── crawler.py # Skill F: XinwenLianbo Crawler
|
||||
├── /data # 本地存储 (Parquet/SQLite)
|
||||
├── /logs # 运行日志
|
||||
├── main.py # 主调度入口
|
||||
└── README.md # 本文件
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术栈约束 (Tech Stack Constraints)
|
||||
|
||||
* **Language:** Python 3.10+
|
||||
* **LLM Interface:** OpenAI SDK (兼容 GLM-4/Claude-3.5)
|
||||
* **Data Source:** `akshare` (A股行情), `crawl4ai` (新闻联播爬虫)
|
||||
* **Data Process:** `pandas`, `numpy`
|
||||
* **Storage:** 本地 JSON 文件模拟 MCP Server (MVP阶段)
|
||||
|
||||
---
|
||||
|
||||
## 7. 给 AI 工具的构建指令 (Instructions for AI Builder)
|
||||
|
||||
1. **Step 1:** 首先创建 `core/` 目录,定义好 `WorldBook` 类和 JSON 数据结构,这是所有 Skill 通信的基础。
|
||||
2. **Step 2:** 实现 `skills/quant.py`,确保能从 Akshare 获取 ETF 数据并计算“资金放量”指标。
|
||||
3. **Step 3:** 实现 `skills/analyst.py`,编写能够将新闻文本转化为 JSON 对象的 Prompt。
|
||||
4. **Step 4:** 实现 `skills/pm.py` 中的 **Trust Index** 算法,确保逻辑与上述公式一致。
|
||||
5. **Step 5:** 编写 `main.py` 将上述模块串联起来,完成每日一次的模拟运行。
|
||||
|
||||
23
requirements.txt
Normal file
23
requirements.txt
Normal file
@ -0,0 +1,23 @@
|
||||
# MND-IA 项目依赖
|
||||
|
||||
# 核心数据处理
|
||||
numpy>=1.24.0
|
||||
pandas>=2.0.0
|
||||
|
||||
# 金融数据获取
|
||||
akshare>=1.12.0
|
||||
|
||||
# 网页爬虫
|
||||
cloudscraper>=1.2.0
|
||||
beautifulsoup4>=4.12.0
|
||||
|
||||
# LLM 接口(可选)
|
||||
openai>=1.0.0
|
||||
|
||||
# 其他工具
|
||||
python-dateutil>=2.8.2
|
||||
requests>=2.31.0
|
||||
|
||||
# 开发工具(可选)
|
||||
jupyter>=1.0.0
|
||||
matplotlib>=3.7.0
|
||||
21
skills/__init__.py
Normal file
21
skills/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""
|
||||
MND-IA Skills Module
|
||||
====================
|
||||
Agent 技能群
|
||||
"""
|
||||
|
||||
from .analyst import NewsAnalyzer
|
||||
from .quant import QuantAnalyzer
|
||||
from .strategist import MacroStrategist
|
||||
from .pm import PortfolioManager
|
||||
from .auditor import PerformanceAuditor
|
||||
from .crawler import XinwenLianboCrawler
|
||||
|
||||
__all__ = [
|
||||
'NewsAnalyzer',
|
||||
'QuantAnalyzer',
|
||||
'MacroStrategist',
|
||||
'PortfolioManager',
|
||||
'PerformanceAuditor',
|
||||
'XinwenLianboCrawler'
|
||||
]
|
||||
582
skills/analyst.py
Normal file
582
skills/analyst.py
Normal file
@ -0,0 +1,582 @@
|
||||
"""
|
||||
Skill A: Analyst (情报分析师)
|
||||
==============================
|
||||
职能:NLP 提取、新闻分级、叙事打分
|
||||
|
||||
输入:新闻文本列表
|
||||
输出:Narrative_JSON (叙事对象、评分、生命周期阶段)
|
||||
|
||||
设计原则:
|
||||
- 核心分析逻辑必须通过 LLM 完成,而非简单关键词匹配
|
||||
- 关键词仅作为 LLM 分析的辅助信息和兜底方案
|
||||
- 支持 LLM 失败时的优雅降级
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from core.config import config, get_llm_client, llm_call
|
||||
|
||||
|
||||
class NewsAnalyzer:
|
||||
"""
|
||||
情报分析师 - 负责新闻文本分析和叙事提取
|
||||
|
||||
核心能力:
|
||||
1. 使用 LLM 进行新闻语义分析(主要方法)
|
||||
2. 新闻分级(Level A/B/C)
|
||||
3. 叙事主题提取与关联
|
||||
4. 热度评分与市场影响判断
|
||||
5. 生命周期阶段推断
|
||||
|
||||
分析流程:
|
||||
1. 首先尝试使用 LLM 进行深度分析
|
||||
2. LLM 返回结构化的分析结果
|
||||
3. 如果 LLM 调用失败,降级到规则引擎
|
||||
"""
|
||||
|
||||
# LLM 分析系统提示词
|
||||
SYSTEM_PROMPT = """你是专业的 A 股市场情报分析师,专门负责解读政策新闻和市场信息。
|
||||
|
||||
你的核心任务是:
|
||||
1. 判断新闻的重要性级别 (A/B/C级)
|
||||
2. 识别受影响的行业板块
|
||||
3. 评估新闻对市场的影响强度
|
||||
4. 判断叙事所处的生命周期阶段
|
||||
|
||||
评级标准:
|
||||
- A级:央行/国务院级别的重大政策(如降准降息、印花税调整、重大改革),通常能引发市场大幅波动
|
||||
- B级:部委级别的行业政策(如工信部新规、发改委规划),对特定行业有显著影响
|
||||
- C级:一般性新闻、行业动态、公司公告,影响有限
|
||||
|
||||
生命周期阶段:
|
||||
- incubation (潜伏期): 政策风向初现,尚未被市场充分认知
|
||||
- fermentation (发酵期): 市场开始关注,资金试探性流入
|
||||
- realization (兑现期): 政策落地,市场充分反应
|
||||
- decay (衰退期): 利好出尽,边际效应递减
|
||||
|
||||
请严格按照 JSON 格式返回分析结果。"""
|
||||
|
||||
def __init__(self, llm_client=None, use_llm: bool = True):
|
||||
"""
|
||||
Args:
|
||||
llm_client: OpenAI SDK 客户端(兼容 GLM/Claude),如果不提供则使用配置文件中的默认客户端
|
||||
use_llm: 是否使用 LLM 进行分析(默认 True)
|
||||
"""
|
||||
self.llm_client = llm_client or get_llm_client()
|
||||
self.use_llm = use_llm and (self.llm_client is not None)
|
||||
self.asset_map = self._load_asset_map()
|
||||
|
||||
# 板块关键词映射 - 仅作为 LLM 的参考信息
|
||||
self.sector_keywords = {}
|
||||
self.sector_names = {} # 资产ID到中文名的映射
|
||||
if self.asset_map:
|
||||
for asset_id, asset_data in self.asset_map.get("assets", {}).items():
|
||||
self.sector_keywords[asset_id] = asset_data.get("keywords", [])
|
||||
self.sector_names[asset_id] = asset_data.get("name", asset_id)
|
||||
|
||||
if self.use_llm:
|
||||
print("[Analyst] ✅ LLM 模式启用 - 将使用大模型进行智能分析")
|
||||
else:
|
||||
print("[Analyst] ⚠️ 降级模式 - 将使用规则引擎(关键词匹配)")
|
||||
|
||||
def _load_asset_map(self) -> Optional[Dict]:
|
||||
"""加载资产映射表"""
|
||||
asset_map_path = Path("core") / "asset_map.json"
|
||||
if not asset_map_path.exists():
|
||||
print("[Analyst] 警告: 未找到 asset_map.json")
|
||||
return None
|
||||
|
||||
with open(asset_map_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def _get_available_sectors_prompt(self) -> str:
|
||||
"""生成可用板块列表的提示信息"""
|
||||
if not self.asset_map:
|
||||
return ""
|
||||
|
||||
sectors_info = []
|
||||
for asset_id, asset_data in self.asset_map.get("assets", {}).items():
|
||||
name = asset_data.get("name", asset_id)
|
||||
keywords = asset_data.get("keywords", [])[:5] # 取前5个关键词
|
||||
sectors_info.append(f"- {asset_id} ({name}): {', '.join(keywords)}")
|
||||
|
||||
return "\n".join(sectors_info)
|
||||
|
||||
def _build_analysis_prompt(self, news_text: str, title: str) -> str:
|
||||
"""构建 LLM 分析的用户提示词"""
|
||||
sectors_info = self._get_available_sectors_prompt()
|
||||
|
||||
return f"""请分析以下新闻,并以 JSON 格式返回结果:
|
||||
|
||||
【新闻标题】
|
||||
{title}
|
||||
|
||||
【新闻正文】
|
||||
{news_text}
|
||||
|
||||
【可用板块列表】
|
||||
{sectors_info}
|
||||
|
||||
请返回如下格式的 JSON(不要包含其他文字):
|
||||
{{
|
||||
"level": "A/B/C",
|
||||
"level_reason": "判断级别的理由(50字内)",
|
||||
"sectors": ["asset_id1", "asset_id2"],
|
||||
"sector_reason": "选择这些板块的理由(50字内)",
|
||||
"score": 0-100,
|
||||
"score_reason": "评分依据(30字内)",
|
||||
"lifecycle_stage": "incubation/fermentation/realization/decay",
|
||||
"stage_reason": "生命周期判断理由(30字内)",
|
||||
"sentiment": "positive/negative/neutral",
|
||||
"key_signal": "新闻中最关键的投资信号(30字内)"
|
||||
}}"""
|
||||
|
||||
def analyze_news(self, news_text: str, title: str = "") -> Dict:
|
||||
"""
|
||||
单条新闻分析(主要方法)
|
||||
|
||||
优先使用 LLM 进行智能分析,失败时降级到规则引擎
|
||||
|
||||
Args:
|
||||
news_text: 新闻正文
|
||||
title: 新闻标题(可选)
|
||||
|
||||
Returns:
|
||||
分析结果字典
|
||||
"""
|
||||
if self.use_llm:
|
||||
result = self._analyze_with_llm(news_text, title)
|
||||
if result:
|
||||
return result
|
||||
print("[Analyst] ⚠️ LLM 分析失败,降级到规则引擎")
|
||||
|
||||
# 降级到规则引擎
|
||||
return self._analyze_with_rules(news_text, title)
|
||||
|
||||
def _analyze_with_llm(self, news_text: str, title: str) -> Optional[Dict]:
|
||||
"""
|
||||
使用 LLM 进行深度新闻分析
|
||||
|
||||
Args:
|
||||
news_text: 新闻正文
|
||||
title: 新闻标题
|
||||
|
||||
Returns:
|
||||
分析结果字典,失败返回 None
|
||||
"""
|
||||
full_text = f"{title}\n{news_text}" if title else news_text
|
||||
|
||||
# 构建提示词
|
||||
user_prompt = self._build_analysis_prompt(news_text, title)
|
||||
|
||||
try:
|
||||
# 调用 LLM
|
||||
llm_output = llm_call(
|
||||
messages=[
|
||||
{"role": "system", "content": self.SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=800
|
||||
)
|
||||
|
||||
if not llm_output:
|
||||
return None
|
||||
|
||||
# 清理并解析 JSON
|
||||
llm_output = llm_output.strip()
|
||||
# 移除可能的 markdown 代码块标记
|
||||
if llm_output.startswith("```"):
|
||||
llm_output = re.sub(r'^```(?:json)?\s*', '', llm_output)
|
||||
llm_output = re.sub(r'\s*```$', '', llm_output)
|
||||
|
||||
llm_result = json.loads(llm_output)
|
||||
|
||||
# 验证并规范化结果
|
||||
level = llm_result.get("level", "C").upper()
|
||||
if level not in ["A", "B", "C"]:
|
||||
level = "C"
|
||||
|
||||
sectors = llm_result.get("sectors", [])
|
||||
# 验证板块是否存在于 asset_map 中
|
||||
valid_sectors = [s for s in sectors if s in self.sector_keywords]
|
||||
|
||||
score = float(llm_result.get("score", 50))
|
||||
score = max(0, min(100, score)) # 限制在 0-100
|
||||
|
||||
lifecycle_stage = llm_result.get("lifecycle_stage", "incubation")
|
||||
if lifecycle_stage not in ["incubation", "fermentation", "realization", "decay"]:
|
||||
lifecycle_stage = "incubation"
|
||||
|
||||
# 关联 ETF
|
||||
related_etfs = []
|
||||
if self.asset_map:
|
||||
for sector in valid_sectors:
|
||||
asset_data = self.asset_map.get("assets", {}).get(sector, {})
|
||||
related_etfs.extend(asset_data.get("etfs", []))
|
||||
related_etfs = list(set(related_etfs))
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"level": level,
|
||||
"level_reason": llm_result.get("level_reason", ""),
|
||||
"sectors": valid_sectors,
|
||||
"sector_reason": llm_result.get("sector_reason", ""),
|
||||
"score": round(score, 2),
|
||||
"score_reason": llm_result.get("score_reason", ""),
|
||||
"lifecycle_stage": lifecycle_stage,
|
||||
"stage_reason": llm_result.get("stage_reason", ""),
|
||||
"related_etfs": related_etfs,
|
||||
"sentiment": llm_result.get("sentiment", "neutral"),
|
||||
"key_signal": llm_result.get("key_signal", ""),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"analysis_method": "llm",
|
||||
"summary": f"{level}级 {len(valid_sectors)}板块 评分{score:.0f} [{llm_result.get('sentiment', 'neutral')}]"
|
||||
}
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[Analyst] LLM 返回格式解析失败: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[Analyst] LLM 分析异常: {e}")
|
||||
return None
|
||||
|
||||
def _analyze_with_rules(self, news_text: str, title: str) -> Dict:
|
||||
"""
|
||||
规则引擎分析(降级方案)
|
||||
|
||||
当 LLM 不可用时,使用关键词匹配进行基础分析
|
||||
|
||||
Args:
|
||||
news_text: 新闻正文
|
||||
title: 新闻标题
|
||||
|
||||
Returns:
|
||||
分析结果字典
|
||||
"""
|
||||
full_text = f"{title}\n{news_text}" if title else news_text
|
||||
|
||||
# 1. 新闻分级(关键词匹配)
|
||||
level = self._classify_level_by_keywords(full_text)
|
||||
|
||||
# 2. 板块识别(关键词匹配)
|
||||
sectors = self._extract_sectors_by_keywords(full_text)
|
||||
|
||||
# 3. 评分计算
|
||||
score = self._calculate_score_by_rules(full_text, level, sectors)
|
||||
|
||||
# 4. 生命周期阶段
|
||||
stage = self._determine_stage_by_rules(level, score)
|
||||
|
||||
# 5. 关联 ETF
|
||||
related_etfs = []
|
||||
if self.asset_map:
|
||||
for sector in sectors:
|
||||
asset_data = self.asset_map.get("assets", {}).get(sector, {})
|
||||
related_etfs.extend(asset_data.get("etfs", []))
|
||||
related_etfs = list(set(related_etfs))
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"level": level,
|
||||
"level_reason": "基于关键词匹配(降级模式)",
|
||||
"sectors": sectors,
|
||||
"sector_reason": "基于关键词匹配(降级模式)",
|
||||
"score": score,
|
||||
"score_reason": "规则引擎计算",
|
||||
"lifecycle_stage": stage,
|
||||
"stage_reason": "基于级别和评分推断",
|
||||
"related_etfs": related_etfs,
|
||||
"sentiment": "neutral",
|
||||
"key_signal": "",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"analysis_method": "rules",
|
||||
"summary": f"{level}级 {len(sectors)}板块 评分{score} [降级模式]"
|
||||
}
|
||||
|
||||
# ==================== 规则引擎辅助方法(降级方案) ====================
|
||||
|
||||
# 新闻分级关键词库
|
||||
LEVEL_A_KEYWORDS = [
|
||||
"印花税", "降准", "降息", "降低存款准备金率",
|
||||
"央行宣布", "国务院", "重磅", "历史性", "里程碑",
|
||||
"全面降息", "万亿", "大规模刺激", "救市", "重大改革"
|
||||
]
|
||||
|
||||
LEVEL_B_KEYWORDS = [
|
||||
"部委", "指导意见", "规划", "通知",
|
||||
"发改委", "工信部", "证监会", "银保监会",
|
||||
"新政", "改革", "试点", "专项", "行动计划"
|
||||
]
|
||||
|
||||
def _classify_level_by_keywords(self, text: str) -> str:
|
||||
"""基于关键词的新闻分级"""
|
||||
for keyword in self.LEVEL_A_KEYWORDS:
|
||||
if keyword in text:
|
||||
return "A"
|
||||
for keyword in self.LEVEL_B_KEYWORDS:
|
||||
if keyword in text:
|
||||
return "B"
|
||||
return "C"
|
||||
|
||||
def _extract_sectors_by_keywords(self, text: str) -> List[str]:
|
||||
"""基于关键词的板块提取"""
|
||||
matched_sectors = []
|
||||
for sector, keywords in self.sector_keywords.items():
|
||||
for keyword in keywords:
|
||||
if keyword in text:
|
||||
matched_sectors.append(sector)
|
||||
break
|
||||
return matched_sectors
|
||||
|
||||
def _calculate_score_by_rules(self, text: str, level: str, sectors: List[str]) -> float:
|
||||
"""基于规则的评分计算"""
|
||||
base_score = {"A": 85, "B": 65, "C": 45}.get(level, 45)
|
||||
sector_bonus = min(len(sectors) * 5, 15)
|
||||
|
||||
# 情绪词检测
|
||||
positive_keywords = ["利好", "刺激", "支持", "鼓励", "加快", "推动", "大力"]
|
||||
emotion_bonus = 10 if any(kw in text for kw in positive_keywords) else 0
|
||||
|
||||
return min(base_score + sector_bonus + emotion_bonus, 100)
|
||||
|
||||
def _determine_stage_by_rules(self, level: str, score: float) -> str:
|
||||
"""基于规则的生命周期阶段判断"""
|
||||
if level == "A" and score >= 80:
|
||||
return "realization"
|
||||
elif level in ["A", "B"] and score >= 60:
|
||||
return "fermentation"
|
||||
else:
|
||||
return "incubation"
|
||||
|
||||
def batch_analyze(
|
||||
self,
|
||||
news_list: List[Dict[str, str]]
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
批量分析新闻
|
||||
|
||||
Args:
|
||||
news_list: 新闻列表,每条包含 {"title": "", "text": ""}
|
||||
|
||||
Returns:
|
||||
分析结果列表
|
||||
"""
|
||||
results = []
|
||||
llm_count = 0
|
||||
rules_count = 0
|
||||
|
||||
mode = "LLM智能分析" if self.use_llm else "规则引擎(降级模式)"
|
||||
print(f"[Analyst] 开始批量分析 {len(news_list)} 条新闻 | 模式: {mode}")
|
||||
|
||||
for i, news in enumerate(news_list, 1):
|
||||
title = news.get("title", "")
|
||||
text = news.get("text", "")
|
||||
|
||||
# 简化标题显示
|
||||
display_title = title[:25] + "..." if len(title) > 25 else title
|
||||
|
||||
result = self.analyze_news(text, title)
|
||||
results.append(result)
|
||||
|
||||
# 统计分析方法使用情况
|
||||
if result.get("analysis_method") == "llm":
|
||||
llm_count += 1
|
||||
method_icon = "🤖"
|
||||
else:
|
||||
rules_count += 1
|
||||
method_icon = "📏"
|
||||
|
||||
print(f"[Analyst] {method_icon} ({i}/{len(news_list)}) {result['level']}级 | {result['score']:.0f}分 | {display_title}")
|
||||
|
||||
print(f"[Analyst] ✅ 批量分析完成 | LLM: {llm_count}条, 规则: {rules_count}条")
|
||||
return results
|
||||
|
||||
def generate_narrative_json(
|
||||
self,
|
||||
analysis_results: List[Dict]
|
||||
) -> Dict:
|
||||
"""
|
||||
生成标准化的 Narrative_JSON(供 Strategist 使用)
|
||||
|
||||
聚合逻辑:
|
||||
1. 按板块(sector)聚合所有相关新闻
|
||||
2. 计算平均分和最高分
|
||||
3. 汇总 LLM 分析的关键信号
|
||||
4. 确定最终的生命周期阶段
|
||||
|
||||
Args:
|
||||
analysis_results: batch_analyze 的输出
|
||||
|
||||
Returns:
|
||||
标准化 JSON 格式
|
||||
"""
|
||||
# 按板块聚合新闻
|
||||
sector_narratives = {}
|
||||
|
||||
for result in analysis_results:
|
||||
for sector in result['sectors']:
|
||||
if sector not in sector_narratives:
|
||||
sector_narratives[sector] = {
|
||||
"topic": sector,
|
||||
"topic_name": self.sector_names.get(sector, sector),
|
||||
"news_count": 0,
|
||||
"avg_score": 0,
|
||||
"max_score": 0,
|
||||
"level_a_count": 0,
|
||||
"level_b_count": 0,
|
||||
"related_etfs": set(),
|
||||
"lifecycle_stage": "incubation",
|
||||
"key_signals": [], # 汇总关键信号
|
||||
"sentiments": [], # 情绪统计
|
||||
"analysis_methods": {"llm": 0, "rules": 0}
|
||||
}
|
||||
|
||||
narrative = sector_narratives[sector]
|
||||
narrative["news_count"] += 1
|
||||
narrative["avg_score"] += result['score']
|
||||
narrative["max_score"] = max(narrative["max_score"], result['score'])
|
||||
narrative["related_etfs"].update(result['related_etfs'])
|
||||
|
||||
# 统计新闻级别
|
||||
if result['level'] == "A":
|
||||
narrative["level_a_count"] += 1
|
||||
narrative["lifecycle_stage"] = result['lifecycle_stage']
|
||||
elif result['level'] == "B":
|
||||
narrative["level_b_count"] += 1
|
||||
if narrative["lifecycle_stage"] == "incubation":
|
||||
narrative["lifecycle_stage"] = "fermentation"
|
||||
|
||||
# 收集关键信号(来自 LLM 分析)
|
||||
if result.get("key_signal"):
|
||||
narrative["key_signals"].append(result["key_signal"])
|
||||
|
||||
# 收集情绪
|
||||
sentiment = result.get("sentiment", "neutral")
|
||||
narrative["sentiments"].append(sentiment)
|
||||
|
||||
# 统计分析方法
|
||||
method = result.get("analysis_method", "rules")
|
||||
narrative["analysis_methods"][method] = narrative["analysis_methods"].get(method, 0) + 1
|
||||
|
||||
# 后处理:计算平均分、汇总情绪等
|
||||
for narrative in sector_narratives.values():
|
||||
narrative["avg_score"] = round(
|
||||
narrative["avg_score"] / narrative["news_count"], 2
|
||||
)
|
||||
narrative["related_etfs"] = list(narrative["related_etfs"])
|
||||
|
||||
# 计算情绪倾向
|
||||
sentiments = narrative["sentiments"]
|
||||
positive_count = sentiments.count("positive")
|
||||
negative_count = sentiments.count("negative")
|
||||
if positive_count > negative_count:
|
||||
narrative["overall_sentiment"] = "positive"
|
||||
elif negative_count > positive_count:
|
||||
narrative["overall_sentiment"] = "negative"
|
||||
else:
|
||||
narrative["overall_sentiment"] = "neutral"
|
||||
|
||||
# 去重关键信号
|
||||
narrative["key_signals"] = list(set(narrative["key_signals"]))[:5]
|
||||
|
||||
# 清理临时字段
|
||||
del narrative["sentiments"]
|
||||
|
||||
# 排序输出
|
||||
sorted_narratives = sorted(
|
||||
sector_narratives.values(),
|
||||
key=lambda x: x['max_score'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"total_news": len(analysis_results),
|
||||
"narratives": sorted_narratives
|
||||
}
|
||||
|
||||
|
||||
# ==================== 测试代码 ====================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Skill A: Analyst 情报分析师测试")
|
||||
print("=" * 60)
|
||||
|
||||
# 创建分析器实例(默认启用 LLM)
|
||||
analyst = NewsAnalyzer()
|
||||
|
||||
# 测试新闻样本
|
||||
test_news = [
|
||||
{
|
||||
"title": "央行宣布降准0.5个百分点 释放长期资金约1万亿",
|
||||
"text": "中国人民银行决定于2025年1月5日降低金融机构存款准备金率0.5个百分点,"
|
||||
"此次降准为全面降准,除已执行5%存款准备金率的部分法人金融机构外,"
|
||||
"对其他金融机构普遍下调存款准备金率0.5个百分点,此次降准共计释放长期资金约1万亿元。"
|
||||
},
|
||||
{
|
||||
"title": "工信部发布低空经济发展指导意见 推动eVTOL产业化",
|
||||
"text": "工业和信息化部发布《低空经济高质量发展指导意见》,提出到2027年,"
|
||||
"低空经济规模达到1万亿元,培育10家以上龙头企业。重点支持无人机、"
|
||||
"电动垂直起降飞行器(eVTOL)等装备的研发和产业化。"
|
||||
},
|
||||
{
|
||||
"title": "AI算力需求持续旺盛 多家数据中心公司业绩预增",
|
||||
"text": "随着人工智能应用的快速发展,算力需求呈现爆发式增长。多家上市公司发布业绩预告,"
|
||||
"预计2024年净利润增长50%以上。分析师认为,智算中心建设将成为未来3年的投资主线。"
|
||||
}
|
||||
]
|
||||
|
||||
# 批量分析
|
||||
print("\n")
|
||||
results = analyst.batch_analyze(test_news)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("详细分析结果:")
|
||||
print("=" * 60)
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f"\n【新闻 {i}】{result['title'][:50]}")
|
||||
print(f" 📊 分析方法: {result.get('analysis_method', 'unknown').upper()}")
|
||||
print(f" 📌 级别: {result['level']} | 评分: {result['score']}")
|
||||
print(f" 🎯 板块: {', '.join(result['sectors']) if result['sectors'] else '未识别'}")
|
||||
print(f" 🔄 周期阶段: {result['lifecycle_stage']}")
|
||||
print(f" 📈 情绪: {result.get('sentiment', 'neutral')}")
|
||||
if result.get('key_signal'):
|
||||
print(f" 💡 关键信号: {result['key_signal']}")
|
||||
if result.get('level_reason'):
|
||||
print(f" 📝 级别理由: {result['level_reason']}")
|
||||
print(f" 🏷️ 关联 ETF: {', '.join(result['related_etfs'][:4]) if result['related_etfs'] else '无'}...")
|
||||
|
||||
# 生成叙事 JSON
|
||||
print("\n" + "=" * 60)
|
||||
print("聚合叙事 JSON:")
|
||||
print("=" * 60)
|
||||
|
||||
narrative_json = analyst.generate_narrative_json(results)
|
||||
print(f"\n📰 总新闻数: {narrative_json['total_news']}")
|
||||
print(f"🎯 识别叙事: {len(narrative_json['narratives'])} 个\n")
|
||||
|
||||
for narrative in narrative_json['narratives']:
|
||||
topic_name = narrative.get('topic_name', narrative['topic'])
|
||||
level_info = f"A级×{narrative['level_a_count']}" if narrative['level_a_count'] > 0 else \
|
||||
f"B级×{narrative.get('level_b_count', 0)}" if narrative.get('level_b_count', 0) > 0 else "C级"
|
||||
|
||||
print(f" 📍 {topic_name} ({narrative['topic']})")
|
||||
print(f" 评分: {narrative['max_score']:.0f} (平均{narrative['avg_score']:.0f}) | "
|
||||
f"新闻: {narrative['news_count']}条 | {level_info}")
|
||||
print(f" 阶段: {narrative['lifecycle_stage']} | 情绪: {narrative.get('overall_sentiment', 'neutral')}")
|
||||
if narrative.get('key_signals'):
|
||||
print(f" 信号: {'; '.join(narrative['key_signals'][:2])}")
|
||||
print()
|
||||
|
||||
print("✅ Analyst 模块测试完成")
|
||||
475
skills/auditor.py
Normal file
475
skills/auditor.py
Normal file
@ -0,0 +1,475 @@
|
||||
"""
|
||||
Skill E: Auditor (复盘审计员)
|
||||
==============================
|
||||
职能:绩效归因、偏差修正
|
||||
|
||||
输入:历史预测 vs 实际走势
|
||||
输出:Correction_Report(修正建议)
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from core.config import config, llm_call
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class PerformanceAuditor:
|
||||
"""
|
||||
复盘审计员 - 负责绩效归因和策略修正
|
||||
|
||||
核心职责:
|
||||
1. 对比预测与实际表现
|
||||
2. 分析偏差原因
|
||||
3. 提出修正建议
|
||||
4. 记录经验教训
|
||||
"""
|
||||
|
||||
def __init__(self, data_dir: str = "data"):
|
||||
self.data_dir = Path(data_dir)
|
||||
self.audit_log_path = self.data_dir / "audit_logs.json"
|
||||
self.correction_history = self._load_correction_history()
|
||||
|
||||
def _load_correction_history(self) -> List[Dict]:
|
||||
"""加载历史修正记录"""
|
||||
if not self.audit_log_path.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(self.audit_log_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"[Auditor] 加载修正历史失败: {e}")
|
||||
return []
|
||||
|
||||
def _save_correction_history(self) -> None:
|
||||
"""保存修正记录"""
|
||||
try:
|
||||
with open(self.audit_log_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.correction_history, f, ensure_ascii=False, indent=2)
|
||||
print(f"[Auditor] 修正记录已保存")
|
||||
except Exception as e:
|
||||
print(f"[Auditor] 保存失败: {e}")
|
||||
|
||||
def audit_prediction(
|
||||
self,
|
||||
prediction: Dict,
|
||||
actual_performance: Dict
|
||||
) -> Dict:
|
||||
"""
|
||||
审计单个预测
|
||||
|
||||
Args:
|
||||
prediction: 预测记录
|
||||
{
|
||||
"code": "515980",
|
||||
"trust_index": 75.0,
|
||||
"narrative_score": 85.0,
|
||||
"flow_score": 60.0,
|
||||
"predicted_action": "buy",
|
||||
"timestamp": "2025-12-29T08:30:00"
|
||||
}
|
||||
|
||||
actual_performance: 实际表现
|
||||
{
|
||||
"code": "515980",
|
||||
"price_change": -2.5, # 实际涨跌幅 %
|
||||
"volume_change": 1.3, # 成交量变化倍数
|
||||
"date": "2025-12-29"
|
||||
}
|
||||
|
||||
Returns:
|
||||
审计结果
|
||||
"""
|
||||
code = prediction['code']
|
||||
predicted_action = prediction['predicted_action']
|
||||
trust_index = prediction['trust_index']
|
||||
price_change = actual_performance['price_change']
|
||||
|
||||
# 判断预测是否正确
|
||||
if predicted_action == "buy":
|
||||
is_correct = price_change > 0
|
||||
expected_direction = "上涨"
|
||||
elif predicted_action == "sell":
|
||||
is_correct = price_change < 0
|
||||
expected_direction = "下跌"
|
||||
else:
|
||||
is_correct = abs(price_change) < 2
|
||||
expected_direction = "震荡"
|
||||
|
||||
# 计算偏差程度
|
||||
deviation = abs(price_change) if not is_correct else 0
|
||||
|
||||
# 分析原因
|
||||
reason = self._analyze_deviation_reason(
|
||||
prediction,
|
||||
actual_performance,
|
||||
is_correct
|
||||
)
|
||||
|
||||
# 生成修正建议
|
||||
correction = self._generate_correction(
|
||||
prediction,
|
||||
actual_performance,
|
||||
is_correct,
|
||||
reason
|
||||
)
|
||||
|
||||
audit_result = {
|
||||
"code": code,
|
||||
"prediction": {
|
||||
"action": predicted_action,
|
||||
"trust_index": trust_index,
|
||||
"expected": expected_direction
|
||||
},
|
||||
"actual": {
|
||||
"price_change": price_change,
|
||||
"volume_change": actual_performance.get('volume_change', 1.0)
|
||||
},
|
||||
"result": {
|
||||
"is_correct": is_correct,
|
||||
"deviation": round(deviation, 2),
|
||||
"reason": reason
|
||||
},
|
||||
"correction": correction,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return audit_result
|
||||
|
||||
def _analyze_deviation_reason(
|
||||
self,
|
||||
prediction: Dict,
|
||||
actual: Dict,
|
||||
is_correct: bool
|
||||
) -> str:
|
||||
"""分析偏差原因"""
|
||||
if is_correct:
|
||||
return "预测准确"
|
||||
|
||||
narrative_score = prediction.get('narrative_score', 0)
|
||||
flow_score = prediction.get('flow_score', 0)
|
||||
price_change = actual['price_change']
|
||||
volume_change = actual.get('volume_change', 1.0)
|
||||
|
||||
# 情况1:叙事高但资金未跟进
|
||||
if narrative_score > 70 and flow_score < 40:
|
||||
return "叙事未兑现:新闻热度高但资金流入不足,市场观望情绪浓厚"
|
||||
|
||||
# 情况2:资金放量但方向相反
|
||||
if volume_change > 1.5 and price_change < -2:
|
||||
return "放量下跌:资金大量流出,可能遭遇利空或止盈盘涌出"
|
||||
|
||||
# 情况3:黑天鹅事件
|
||||
if abs(price_change) > 5:
|
||||
return "极端波动:可能受突发事件或外部市场剧烈波动影响"
|
||||
|
||||
# 情况4:叙事过度衰减
|
||||
if narrative_score < 50 and prediction['predicted_action'] == "buy":
|
||||
return "叙事衰减过快:市场关注度下降速度超预期"
|
||||
|
||||
return "其他因素:需进一步分析宏观环境或板块轮动"
|
||||
|
||||
def _generate_correction(
|
||||
self,
|
||||
prediction: Dict,
|
||||
actual: Dict,
|
||||
is_correct: bool,
|
||||
reason: str
|
||||
) -> Dict:
|
||||
"""生成修正建议"""
|
||||
if is_correct:
|
||||
return {
|
||||
"action": "none",
|
||||
"message": "预测准确,保持当前策略"
|
||||
}
|
||||
|
||||
narrative_score = prediction.get('narrative_score', 0)
|
||||
flow_score = prediction.get('flow_score', 0)
|
||||
|
||||
corrections = []
|
||||
|
||||
# 修正1:提高资金流权重
|
||||
if "资金流入不足" in reason:
|
||||
corrections.append({
|
||||
"target": "trust_index_formula",
|
||||
"suggestion": "提高资金流权重至 0.5,降低叙事权重至 0.5",
|
||||
"reason": "叙事未能转化为实际资金流动"
|
||||
})
|
||||
|
||||
# 修正2:加强止损
|
||||
if "放量下跌" in reason:
|
||||
corrections.append({
|
||||
"target": "risk_control",
|
||||
"suggestion": "设置 -3% 硬止损线,出现放量下跌立即清仓",
|
||||
"reason": "未能及时识别资金流出信号"
|
||||
})
|
||||
|
||||
# 修正3:降低 Trust Index 阈值
|
||||
if narrative_score > 60 and flow_score > 50 and not is_correct:
|
||||
corrections.append({
|
||||
"target": "min_trust_score",
|
||||
"suggestion": "提高最低信任指数阈值至 65",
|
||||
"reason": "当前阈值可能过于宽松,需要更高确定性"
|
||||
})
|
||||
|
||||
# 修正4:叙事衰减系数调整
|
||||
if "衰减过快" in reason:
|
||||
corrections.append({
|
||||
"target": "narrative_decay",
|
||||
"suggestion": "对该板块叙事的衰减系数从 0.95 调整为 0.97",
|
||||
"reason": "市场对该主题的持续关注度被低估"
|
||||
})
|
||||
|
||||
return {
|
||||
"action": "adjust" if corrections else "monitor",
|
||||
"corrections": corrections,
|
||||
"priority": "high" if len(corrections) >= 2 else "medium"
|
||||
}
|
||||
|
||||
def batch_audit(
|
||||
self,
|
||||
predictions: List[Dict],
|
||||
actual_performances: Dict[str, Dict]
|
||||
) -> Dict:
|
||||
"""
|
||||
批量审计
|
||||
|
||||
Args:
|
||||
predictions: 预测列表
|
||||
actual_performances: 实际表现字典 {code: performance}
|
||||
|
||||
Returns:
|
||||
汇总审计报告
|
||||
"""
|
||||
print(f"[Auditor] 开始审计 {len(predictions)} 条预测...")
|
||||
|
||||
audit_results = []
|
||||
correct_count = 0
|
||||
|
||||
for prediction in predictions:
|
||||
code = prediction['code']
|
||||
|
||||
if code not in actual_performances:
|
||||
print(f"[Auditor] 警告: 缺少 {code} 的实际数据")
|
||||
continue
|
||||
|
||||
result = self.audit_prediction(
|
||||
prediction,
|
||||
actual_performances[code]
|
||||
)
|
||||
|
||||
audit_results.append(result)
|
||||
|
||||
if result['result']['is_correct']:
|
||||
correct_count += 1
|
||||
|
||||
# 计算准确率
|
||||
accuracy = correct_count / len(audit_results) * 100 if audit_results else 0
|
||||
|
||||
# 统计偏差原因
|
||||
reason_counts = {}
|
||||
for result in audit_results:
|
||||
reason = result['result']['reason']
|
||||
reason_counts[reason] = reason_counts.get(reason, 0) + 1
|
||||
|
||||
# 收集修正建议
|
||||
all_corrections = []
|
||||
for result in audit_results:
|
||||
if result['correction']['action'] != "none":
|
||||
all_corrections.extend(result['correction'].get('corrections', []))
|
||||
|
||||
# 生成汇总报告
|
||||
summary_report = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"audit_period": {
|
||||
"start": min(r['timestamp'] for r in audit_results) if audit_results else None,
|
||||
"end": max(r['timestamp'] for r in audit_results) if audit_results else None
|
||||
},
|
||||
"performance": {
|
||||
"total_predictions": len(audit_results),
|
||||
"correct_predictions": correct_count,
|
||||
"wrong_predictions": len(audit_results) - correct_count,
|
||||
"accuracy": round(accuracy, 2)
|
||||
},
|
||||
"deviation_analysis": reason_counts,
|
||||
"corrections_needed": len(all_corrections),
|
||||
"high_priority_corrections": [
|
||||
c for c in all_corrections
|
||||
if any('止损' in c.get('suggestion', '') for c in all_corrections)
|
||||
],
|
||||
"detailed_results": audit_results
|
||||
}
|
||||
|
||||
# 保存到历史记录
|
||||
self.correction_history.append(summary_report)
|
||||
self._save_correction_history()
|
||||
|
||||
print(f"[Auditor] 审计完成,准确率: {accuracy:.1f}%")
|
||||
return summary_report
|
||||
|
||||
def generate_correction_report(
|
||||
self,
|
||||
audit_summary: Dict
|
||||
) -> str:
|
||||
"""
|
||||
生成人类可读的修正报告
|
||||
|
||||
Args:
|
||||
audit_summary: batch_audit() 的输出
|
||||
|
||||
Returns:
|
||||
Markdown 格式的报告
|
||||
"""
|
||||
accuracy = audit_summary['performance']['accuracy']
|
||||
total = audit_summary['performance']['total_predictions']
|
||||
correct = audit_summary['performance']['correct_predictions']
|
||||
|
||||
report = f"""
|
||||
# 投资决策复盘审计报告
|
||||
|
||||
**生成时间:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||
|
||||
## 一、整体表现
|
||||
|
||||
- **预测数量:** {total} 条
|
||||
- **正确预测:** {correct} 条
|
||||
- **错误预测:** {total - correct} 条
|
||||
- **准确率:** {accuracy:.1f}%
|
||||
|
||||
## 二、偏差原因分析
|
||||
|
||||
"""
|
||||
for reason, count in audit_summary['deviation_analysis'].items():
|
||||
report += f"- **{reason}:** {count} 次\n"
|
||||
|
||||
report += f"\n## 三、修正建议\n\n"
|
||||
report += f"需要修正的参数: {audit_summary['corrections_needed']} 项\n\n"
|
||||
|
||||
# 列出高优先级修正
|
||||
high_priority = audit_summary.get('high_priority_corrections', [])
|
||||
if high_priority:
|
||||
report += "### 🚨 高优先级修正\n\n"
|
||||
for i, correction in enumerate(high_priority[:3], 1):
|
||||
report += f"{i}. **{correction['target']}**\n"
|
||||
report += f" - 建议: {correction['suggestion']}\n"
|
||||
report += f" - 原因: {correction['reason']}\n\n"
|
||||
|
||||
report += "\n## 四、经验总结\n\n"
|
||||
|
||||
if accuracy >= 70:
|
||||
report += "✅ 当前策略表现良好,建议保持现有参数设置。\n"
|
||||
elif accuracy >= 50:
|
||||
report += "⚠️ 策略有效但存在改进空间,建议根据修正建议调整参数。\n"
|
||||
else:
|
||||
report += "❌ 策略表现不佳,需要重大调整或暂停交易,重新评估市场环境。\n"
|
||||
|
||||
return report
|
||||
|
||||
def get_historical_accuracy_trend(self) -> List[Dict]:
|
||||
"""获取历史准确率趋势"""
|
||||
trend = []
|
||||
|
||||
for record in self.correction_history[-10:]: # 最近10次
|
||||
trend.append({
|
||||
"timestamp": record['timestamp'],
|
||||
"accuracy": record['performance']['accuracy'],
|
||||
"total_predictions": record['performance']['total_predictions']
|
||||
})
|
||||
|
||||
return trend
|
||||
|
||||
|
||||
# ==================== 测试代码 ====================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Skill E: Auditor 复盘审计员测试")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建审计员实例
|
||||
auditor = PerformanceAuditor(data_dir="data")
|
||||
|
||||
# 模拟昨日的预测记录
|
||||
mock_predictions = [
|
||||
{
|
||||
"code": "515980",
|
||||
"trust_index": 75.0,
|
||||
"narrative_score": 85.0,
|
||||
"flow_score": 60.0,
|
||||
"predicted_action": "buy",
|
||||
"timestamp": "2025-12-29T08:30:00"
|
||||
},
|
||||
{
|
||||
"code": "512480",
|
||||
"trust_index": 45.0,
|
||||
"narrative_score": 55.0,
|
||||
"flow_score": 30.0,
|
||||
"predicted_action": "hold",
|
||||
"timestamp": "2025-12-29T08:30:00"
|
||||
},
|
||||
{
|
||||
"code": "159928",
|
||||
"trust_index": 35.0,
|
||||
"narrative_score": 70.0,
|
||||
"flow_score": 20.0,
|
||||
"predicted_action": "sell",
|
||||
"timestamp": "2025-12-29T08:30:00"
|
||||
}
|
||||
]
|
||||
|
||||
# 模拟今日的实际表现
|
||||
mock_actual = {
|
||||
"515980": {
|
||||
"code": "515980",
|
||||
"price_change": 3.2, # 上涨3.2%
|
||||
"volume_change": 1.5,
|
||||
"date": "2025-12-29"
|
||||
},
|
||||
"512480": {
|
||||
"code": "512480",
|
||||
"price_change": -1.8, # 下跌1.8%
|
||||
"volume_change": 0.9,
|
||||
"date": "2025-12-29"
|
||||
},
|
||||
"159928": {
|
||||
"code": "159928",
|
||||
"price_change": 0.5, # 微涨(预测失误)
|
||||
"volume_change": 2.1,
|
||||
"date": "2025-12-29"
|
||||
}
|
||||
}
|
||||
|
||||
# 1. 批量审计
|
||||
print("\n1. 批量审计:")
|
||||
summary = auditor.batch_audit(mock_predictions, mock_actual)
|
||||
|
||||
print(f"\n准确率: {summary['performance']['accuracy']:.1f}%")
|
||||
print(f"正确: {summary['performance']['correct_predictions']}")
|
||||
print(f"错误: {summary['performance']['wrong_predictions']}")
|
||||
|
||||
# 2. 查看详细结果
|
||||
print("\n2. 详细审计结果:")
|
||||
for result in summary['detailed_results']:
|
||||
print(f"\n {result['code']}:")
|
||||
print(f" - 预测: {result['prediction']['action']} (Trust={result['prediction']['trust_index']})")
|
||||
print(f" - 实际: {result['actual']['price_change']:+.1f}%")
|
||||
print(f" - 结果: {'✅ 正确' if result['result']['is_correct'] else '❌ 错误'}")
|
||||
print(f" - 原因: {result['result']['reason']}")
|
||||
|
||||
# 3. 生成修正报告
|
||||
print("\n3. 修正报告:")
|
||||
report = auditor.generate_correction_report(summary)
|
||||
print(report)
|
||||
|
||||
# 4. 历史趋势
|
||||
print("\n4. 历史准确率趋势:")
|
||||
trend = auditor.get_historical_accuracy_trend()
|
||||
for record in trend:
|
||||
print(f" {record['timestamp'][:10]}: {record['accuracy']:.1f}%")
|
||||
|
||||
print("\n✅ Auditor 模块测试完成")
|
||||
298
skills/crawler.py
Normal file
298
skills/crawler.py
Normal file
@ -0,0 +1,298 @@
|
||||
"""
|
||||
Skill F: Crawler (新闻联播爬虫)
|
||||
================================
|
||||
职能:每日抓取新闻联播文字稿
|
||||
|
||||
数据源:https://cn.govopendata.com/xinwenlianbo/
|
||||
输出:符合 Skill A 要求的新闻列表 [{"title": "", "text": ""}]
|
||||
"""
|
||||
|
||||
import cloudscraper
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
from pathlib import Path
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class XinwenLianboCrawler:
|
||||
"""
|
||||
新闻联播爬虫 - 抓取每日新闻联播文字稿
|
||||
|
||||
核心功能:
|
||||
1. 自动构建当日 URL
|
||||
2. 使用 cloudscraper 绕过 Cloudflare 防护
|
||||
3. 解析新闻条目
|
||||
4. 输出符合 Analyst 要求的格式
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str = "https://cn.govopendata.com/xinwenlianbo/"):
|
||||
"""
|
||||
Args:
|
||||
base_url: 新闻联播网站基础 URL
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.cache_dir = Path("data") / "news_cache"
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 创建 cloudscraper 会话
|
||||
self.scraper = cloudscraper.create_scraper(
|
||||
browser={
|
||||
'browser': 'chrome',
|
||||
'platform': 'windows',
|
||||
'desktop': True
|
||||
}
|
||||
)
|
||||
|
||||
def _build_url(self, date: Optional[str] = None) -> str:
|
||||
"""
|
||||
构建目标 URL
|
||||
|
||||
Args:
|
||||
date: 日期字符串 (格式: YYYYMMDD),默认今天
|
||||
|
||||
Returns:
|
||||
完整 URL
|
||||
"""
|
||||
if date is None:
|
||||
date = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
return f"{self.base_url}{date}/"
|
||||
|
||||
def crawl_xinwenlianbo(self, date: Optional[str] = None) -> List[Dict[str, str]]:
|
||||
"""
|
||||
爬取新闻联播
|
||||
|
||||
Args:
|
||||
date: 日期字符串 (YYYYMMDD),默认今天
|
||||
|
||||
Returns:
|
||||
新闻列表 [{"title": "", "text": ""}]
|
||||
"""
|
||||
url = self._build_url(date)
|
||||
display_date = date if date else datetime.now().strftime("%Y%m%d")
|
||||
|
||||
print(f"[Crawler] 开始抓取 {display_date} 新闻联播")
|
||||
print(f"[Crawler] URL: {url}")
|
||||
|
||||
try:
|
||||
# 使用 cloudscraper 发起请求
|
||||
response = self.scraper.get(url, timeout=30)
|
||||
response.raise_for_status()
|
||||
response.encoding = 'utf-8'
|
||||
|
||||
# 解析 HTML
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 提取新闻条目
|
||||
news_list = self._parse_content(soup)
|
||||
|
||||
if not news_list:
|
||||
print(f"[Crawler] 未找到新闻内容,可能网站结构变化或当日无更新")
|
||||
print(f"[Crawler] 页面标题: {soup.title.string if soup.title else 'No Title'}")
|
||||
|
||||
# 尝试从缓存读取
|
||||
cached = self._load_from_cache(display_date)
|
||||
if cached:
|
||||
print(f"[Crawler] 使用缓存数据 ({len(cached)} 条)")
|
||||
return cached
|
||||
|
||||
return []
|
||||
|
||||
print(f"[Crawler] 成功提取 {len(news_list)} 条新闻")
|
||||
|
||||
# 缓存结果
|
||||
self._cache_news(news_list, display_date)
|
||||
|
||||
return news_list
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Crawler] 爬取失败: {e}")
|
||||
|
||||
# 尝试从缓存读取
|
||||
cached = self._load_from_cache(display_date)
|
||||
if cached:
|
||||
print(f"[Crawler] 使用缓存数据 ({len(cached)} 条)")
|
||||
return cached
|
||||
|
||||
return []
|
||||
|
||||
def _parse_content(self, soup: BeautifulSoup) -> List[Dict[str, str]]:
|
||||
"""
|
||||
解析网页内容,提取新闻条目
|
||||
|
||||
网站结构:
|
||||
- 容器: .content-section
|
||||
- 标题: .content-heading
|
||||
- 内容: .content-body
|
||||
|
||||
Args:
|
||||
soup: BeautifulSoup 对象
|
||||
|
||||
Returns:
|
||||
新闻列表
|
||||
"""
|
||||
news_list = []
|
||||
|
||||
# 查找所有新闻板块
|
||||
sections = soup.select('.content-section')
|
||||
|
||||
if not sections:
|
||||
print("[Crawler] 未找到 .content-section 元素")
|
||||
return []
|
||||
|
||||
for section in sections:
|
||||
# 提取标题
|
||||
title_tag = section.select_one('.content-heading')
|
||||
if not title_tag:
|
||||
continue
|
||||
|
||||
title = title_tag.get_text(strip=True)
|
||||
|
||||
# 提取内容
|
||||
content_tag = section.select_one('.content-body')
|
||||
if not content_tag:
|
||||
continue
|
||||
|
||||
# 尝试提取段落以保持格式
|
||||
paragraphs = content_tag.find_all('p')
|
||||
if paragraphs:
|
||||
# 多段落用换行分隔
|
||||
content = ' '.join([p.get_text(strip=True) for p in paragraphs])
|
||||
else:
|
||||
content = content_tag.get_text(strip=True)
|
||||
|
||||
# 过滤过短的内容
|
||||
if len(content) < 10:
|
||||
continue
|
||||
|
||||
news_list.append({
|
||||
"title": title,
|
||||
"text": content
|
||||
})
|
||||
|
||||
return news_list
|
||||
|
||||
def _cache_news(self, news_list: List[Dict], date: str) -> None:
|
||||
"""缓存新闻到本地"""
|
||||
cache_file = self.cache_dir / f"xinwenlianbo_{date}.json"
|
||||
|
||||
try:
|
||||
with open(cache_file, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
"date": date,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"count": len(news_list),
|
||||
"news": news_list
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"[Crawler] 已缓存到 {cache_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Crawler] 缓存失败: {e}")
|
||||
|
||||
def _load_from_cache(self, date: str) -> Optional[List[Dict]]:
|
||||
"""从缓存加载新闻"""
|
||||
cache_file = self.cache_dir / f"xinwenlianbo_{date}.json"
|
||||
|
||||
if not cache_file.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(cache_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
return data.get('news', [])
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Crawler] 读取缓存失败: {e}")
|
||||
return None
|
||||
|
||||
def crawl_sync(
|
||||
self,
|
||||
date: Optional[str] = None
|
||||
) -> List[Dict[str, str]]:
|
||||
"""
|
||||
同步爬取接口(主要方法)
|
||||
|
||||
Args:
|
||||
date: 日期字符串 (YYYYMMDD)
|
||||
|
||||
Returns:
|
||||
新闻列表
|
||||
"""
|
||||
return self.crawl_xinwenlianbo(date)
|
||||
|
||||
def crawl_date_range(
|
||||
self,
|
||||
start_date: str,
|
||||
end_date: str
|
||||
) -> Dict[str, List[Dict]]:
|
||||
"""
|
||||
批量爬取日期范围内的新闻
|
||||
|
||||
Args:
|
||||
start_date: 开始日期 (YYYYMMDD)
|
||||
end_date: 结束日期 (YYYYMMDD)
|
||||
|
||||
Returns:
|
||||
{date: news_list} 字典
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
start = datetime.strptime(start_date, "%Y%m%d")
|
||||
end = datetime.strptime(end_date, "%Y%m%d")
|
||||
|
||||
results = {}
|
||||
current = start
|
||||
|
||||
while current <= end:
|
||||
date_str = current.strftime("%Y%m%d")
|
||||
print(f"\n[Crawler] 处理 {date_str}...")
|
||||
|
||||
news_list = self.crawl_sync(date_str)
|
||||
results[date_str] = news_list
|
||||
|
||||
current += timedelta(days=1)
|
||||
|
||||
# 礼貌延迟,避免频繁请求
|
||||
time.sleep(2)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ==================== 测试代码 ====================
|
||||
|
||||
def test_sync():
|
||||
"""同步测试(用于命令行直接运行)"""
|
||||
print("=" * 50)
|
||||
print("Skill F: Crawler 新闻联播爬虫测试")
|
||||
print("=" * 50)
|
||||
|
||||
crawler = XinwenLianboCrawler()
|
||||
|
||||
# 同步爬取今日新闻
|
||||
print("\n测试 1: 爬取今日新闻")
|
||||
news_list = crawler.crawl_sync()
|
||||
|
||||
print(f"\n获取 {len(news_list)} 条新闻")
|
||||
|
||||
if news_list:
|
||||
print("\n前 3 条新闻预览:")
|
||||
for i, news in enumerate(news_list[:3], 1):
|
||||
print(f"\n[{i}] {news['title']}")
|
||||
print(f" {news['text'][:150]}...")
|
||||
|
||||
# 测试指定日期
|
||||
print("\n" + "=" * 50)
|
||||
print("测试 2: 爬取指定日期 (20251229)")
|
||||
specific_news = crawler.crawl_sync(date="20251229")
|
||||
print(f"获取 {len(specific_news)} 条新闻")
|
||||
|
||||
print("\n✅ Crawler 模块测试完成")
|
||||
return news_list
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_sync()
|
||||
682
skills/pm.py
Normal file
682
skills/pm.py
Normal file
@ -0,0 +1,682 @@
|
||||
"""
|
||||
Skill D: PM (基金经理)
|
||||
========================
|
||||
职能:计算信任指数、风控、生成订单
|
||||
|
||||
输入:World_Book + Market_Data_JSON
|
||||
输出:Trade_Orders (买卖指令列表)
|
||||
|
||||
设计原则:
|
||||
- 核心决策通过量化模型完成(向量点积)
|
||||
- 使用 LLM 提供辅助分析和投资建议
|
||||
- 支持 LLM 失败时的优雅降级
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from core.world_book import WorldBook
|
||||
from core.config import config, llm_call
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
class PortfolioManager:
|
||||
"""
|
||||
基金经理 - 负责投资决策和订单生成
|
||||
|
||||
核心职责:
|
||||
1. 计算 Trust Index(叙事 × 资金流 交叉验证)
|
||||
2. 使用 LLM 提供投资洞察和风险提示
|
||||
3. 风险控制(仓位管理、止损)
|
||||
4. 生成具体的买卖订单
|
||||
"""
|
||||
|
||||
# LLM 投资分析提示词
|
||||
INVESTMENT_ANALYSIS_PROMPT = """你是一位专业的 A 股 ETF 基金经理,擅长根据宏观环境和市场数据做出投资决策。
|
||||
|
||||
你的核心任务是:
|
||||
1. 分析当前宏观环境对各板块的影响
|
||||
2. 评估投资机会的风险收益比
|
||||
3. 给出具体的投资建议和理由
|
||||
4. 提示潜在的风险因素
|
||||
|
||||
请严格按照 JSON 格式返回分析结果。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
world_book: WorldBook,
|
||||
total_capital: float = 1000000.0, # 默认100万
|
||||
max_position_pct: float = 0.15, # 单个 ETF 最大仓位15%
|
||||
min_trust_score: float = 60.0, # 最低信任指数阈值
|
||||
use_llm: bool = True
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
world_book: WorldBook 实例
|
||||
total_capital: 总资金(元)
|
||||
max_position_pct: 单个 ETF 最大仓位比例
|
||||
min_trust_score: 最低信任指数(低于此值不开仓)
|
||||
use_llm: 是否使用 LLM 进行辅助分析
|
||||
"""
|
||||
self.wb = world_book
|
||||
self.total_capital = total_capital
|
||||
self.max_position_pct = max_position_pct
|
||||
self.min_trust_score = min_trust_score
|
||||
self.use_llm = use_llm
|
||||
|
||||
# 加载资产映射表
|
||||
self.asset_map = self._load_asset_map()
|
||||
|
||||
# 当前持仓(简化版,实际应从数据库读取)
|
||||
self.current_positions: Dict[str, Dict] = {}
|
||||
|
||||
if self.use_llm:
|
||||
print("[PM] ✅ LLM 辅助分析启用 - 将提供智能投资洞察")
|
||||
else:
|
||||
print("[PM] ⚠️ 纯量化模式 - 仅使用数学模型")
|
||||
|
||||
def _load_asset_map(self) -> Dict:
|
||||
"""加载资产映射表"""
|
||||
from pathlib import Path
|
||||
asset_map_path = Path("core") / "asset_map.json"
|
||||
if not asset_map_path.exists():
|
||||
print("[PM] 警告: 未找到 asset_map.json")
|
||||
return {}
|
||||
|
||||
with open(asset_map_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def calculate_macro_sensitivity_score(self, asset_id: str) -> float:
|
||||
"""
|
||||
使用向量点积计算资产的宏观敏感度得分
|
||||
|
||||
这是新的核心计算方法,替代传统的手工评分。
|
||||
|
||||
公式:
|
||||
Score = Σ (macro_factor_value[i] × sensitivity[i])
|
||||
|
||||
示例:
|
||||
- 宏观环境向量:{"interest_rate_down": 1.0, "geopolitics_tension": 0.5, "policy_digital_economy": 1.0}
|
||||
- 软件ETF敏感度:{"policy_digital_economy": 1.0, "interest_rate_down": 0.7}
|
||||
- 计算:(1.0 × 1.0) + (1.0 × 0.7) + (0.5 × 0) = 1.7分
|
||||
|
||||
Args:
|
||||
asset_id: 资产ID(例如 "tech_software")
|
||||
|
||||
Returns:
|
||||
宏观敏感度得分(可能为负,表示利空)
|
||||
"""
|
||||
# 获取资产的敏感度矩阵
|
||||
asset_data = self.asset_map.get("assets", {}).get(asset_id, {})
|
||||
sensitivity_matrix = asset_data.get("sensitivity", {})
|
||||
|
||||
if not sensitivity_matrix:
|
||||
print(f"[PM] 警告: {asset_id} 没有敏感度数据")
|
||||
return 0.0
|
||||
|
||||
# 获取当前宏观因子向量
|
||||
macro_vector = self.wb.macro_factor_vector
|
||||
|
||||
if not macro_vector:
|
||||
print("[PM] 警告: 宏观因子向量为空,无法计算")
|
||||
return 0.0
|
||||
|
||||
# 向量点积计算
|
||||
score = 0.0
|
||||
matched_factors = []
|
||||
|
||||
for factor_name, sensitivity_value in sensitivity_matrix.items():
|
||||
macro_value = macro_vector.get(factor_name, 0.0)
|
||||
|
||||
if macro_value != 0:
|
||||
contribution = macro_value * sensitivity_value
|
||||
score += contribution
|
||||
matched_factors.append(f"{factor_name}({macro_value}×{sensitivity_value}={contribution:.2f})")
|
||||
|
||||
print(f"[PM] {asset_id} 宏观得分: {score:.2f} | 因子: {', '.join(matched_factors) if matched_factors else '无匹配'}")
|
||||
|
||||
return round(score, 2)
|
||||
|
||||
def calculate_trust_index(
|
||||
self,
|
||||
etf_code: str,
|
||||
asset_id: str,
|
||||
narrative_score: float,
|
||||
flow_score: float
|
||||
) -> Dict:
|
||||
"""
|
||||
计算 Trust Index(信任指数)- 新版本
|
||||
|
||||
公式(升级版):
|
||||
TrustIndex = (MacroScore × 50 + Narrative × 0.3 + Flow × 0.2) - Penalty
|
||||
|
||||
其中:
|
||||
- MacroScore: 宏观敏感度得分(向量点积)
|
||||
- Narrative: 叙事强度 (0-100)
|
||||
- Flow: 资金流评分 (0-100)
|
||||
|
||||
一票否决规则:
|
||||
- 如果 MacroScore < -1.0,Penalty = 100(宏观环境严重利空)
|
||||
|
||||
Args:
|
||||
etf_code: ETF 代码
|
||||
asset_id: 资产ID
|
||||
narrative_score: 叙事评分 (0-100)
|
||||
flow_score: 资金流评分 (0-100)
|
||||
|
||||
Returns:
|
||||
Trust Index 结果字典
|
||||
"""
|
||||
# 1. 计算宏观敏感度得分
|
||||
macro_score = self.calculate_macro_sensitivity_score(asset_id)
|
||||
|
||||
# 2. 归一化到 0-100 范围(假设 macro_score 在 -2 到 +3 之间)
|
||||
normalized_macro = max(0, min(100, (macro_score + 2) / 5 * 100))
|
||||
|
||||
# 3. 加权计算基础得分
|
||||
base_score = normalized_macro * 0.5 + narrative_score * 0.3 + flow_score * 0.2
|
||||
|
||||
# 4. 一票否决:宏观环境严重利空
|
||||
penalty = 0
|
||||
if macro_score < -1.0:
|
||||
penalty = 100
|
||||
verdict = "reject"
|
||||
else:
|
||||
# 正常评判
|
||||
trust_index = base_score - penalty
|
||||
|
||||
if trust_index >= self.min_trust_score:
|
||||
verdict = "buy"
|
||||
elif trust_index >= 40:
|
||||
verdict = "hold"
|
||||
else:
|
||||
verdict = "sell"
|
||||
|
||||
final_trust_index = max(0, base_score - penalty)
|
||||
|
||||
return {
|
||||
"code": etf_code,
|
||||
"asset_id": asset_id,
|
||||
"trust_index": round(final_trust_index, 2),
|
||||
"macro_score": macro_score,
|
||||
"normalized_macro": round(normalized_macro, 2),
|
||||
"narrative_score": round(narrative_score, 2),
|
||||
"flow_score": round(flow_score, 2),
|
||||
"penalty": penalty,
|
||||
"verdict": verdict,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def batch_calculate_trust_index(
|
||||
self,
|
||||
market_data: Dict[str, Dict]
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
批量计算所有 ETF 的 Trust Index - 新版本
|
||||
|
||||
逻辑:
|
||||
1. 遍历所有资产
|
||||
2. 对每个资产下的 ETF 计算宏观敏感度得分
|
||||
3. 结合叙事和资金流计算最终 Trust Index
|
||||
|
||||
Args:
|
||||
market_data: Quant.batch_analyze() 的输出
|
||||
|
||||
Returns:
|
||||
Trust Index 列表,按评分排序
|
||||
"""
|
||||
results = []
|
||||
|
||||
print(f"[PM] 计算 Trust Index(使用向量点积方法)...")
|
||||
|
||||
# 遍历所有资产
|
||||
for asset_id, asset_data in self.asset_map.get("assets", {}).items():
|
||||
etf_list = asset_data.get("etfs", [])
|
||||
|
||||
for etf_code in etf_list:
|
||||
# 跳过没有市场数据的 ETF
|
||||
if etf_code not in market_data:
|
||||
continue
|
||||
|
||||
# 获取叙事评分
|
||||
narratives = self.wb.get_narratives_by_etf(etf_code)
|
||||
|
||||
if narratives:
|
||||
# 取最高权重的叙事
|
||||
narrative_score = max(n.current_weight for n in narratives)
|
||||
else:
|
||||
narrative_score = 0
|
||||
|
||||
# 获取资金流评分
|
||||
flow_score = market_data[etf_code].get("flow_analysis", {}).get("flow_score", 0)
|
||||
|
||||
# 计算 Trust Index(使用向量点积)
|
||||
trust_result = self.calculate_trust_index(
|
||||
etf_code,
|
||||
asset_id,
|
||||
narrative_score,
|
||||
flow_score
|
||||
)
|
||||
|
||||
results.append(trust_result)
|
||||
|
||||
# 按 Trust Index 排序
|
||||
results.sort(key=lambda x: x['trust_index'], reverse=True)
|
||||
|
||||
print(f"[PM] Trust Index 计算完成,共 {len(results)} 个标的")
|
||||
|
||||
# 打印 Top 5
|
||||
print("\n[PM] Top 5 标的:")
|
||||
for i, result in enumerate(results[:5], 1):
|
||||
asset_name = self.asset_map.get("assets", {}).get(result['asset_id'], {}).get("name", "未知")
|
||||
print(f" {i}. {result['code']} ({asset_name}) - TrustIndex: {result['trust_index']} "
|
||||
f"(Macro: {result['macro_score']:.2f}, Narrative: {result['narrative_score']:.1f}, "
|
||||
f"Flow: {result['flow_score']:.1f})")
|
||||
|
||||
# 使用 LLM 提供投资洞察
|
||||
if self.use_llm and results:
|
||||
self._provide_llm_insights(results[:10])
|
||||
|
||||
return results
|
||||
|
||||
def _provide_llm_insights(self, top_results: List[Dict]) -> None:
|
||||
"""使用 LLM 提供投资洞察"""
|
||||
try:
|
||||
# 准备分析数据
|
||||
opportunities = []
|
||||
for r in top_results:
|
||||
asset_name = self.asset_map.get("assets", {}).get(r['asset_id'], {}).get("name", r['asset_id'])
|
||||
opportunities.append({
|
||||
"code": r['code'],
|
||||
"name": asset_name,
|
||||
"trust_index": r['trust_index'],
|
||||
"macro_score": r['macro_score'],
|
||||
"narrative_score": r['narrative_score'],
|
||||
"flow_score": r['flow_score'],
|
||||
"verdict": r['verdict']
|
||||
})
|
||||
|
||||
# 获取当前宏观状态
|
||||
macro_state = self.wb.macro_cycle.to_dict()
|
||||
macro_factors = list(self.wb.macro_factor_vector.keys())[:5]
|
||||
|
||||
prompt = f"""请分析以下 ETF 投资机会并给出简要建议:
|
||||
|
||||
【当前宏观环境】
|
||||
- 市场周期: {macro_state['status']}
|
||||
- 流动性: {macro_state['liquidity']}
|
||||
- 政策风向: {macro_state['policy_wind']}
|
||||
- 活跃因子: {', '.join(macro_factors) if macro_factors else '无'}
|
||||
|
||||
【Top 投资机会】
|
||||
{json.dumps(opportunities, ensure_ascii=False, indent=2)}
|
||||
|
||||
请以 JSON 格式返回:
|
||||
{{
|
||||
"market_view": "当前市场整体观点(30字内)",
|
||||
"top_pick": "最推荐的标的代码",
|
||||
"top_pick_reason": "推荐理由(50字内)",
|
||||
"risk_warning": "主要风险提示(30字内)",
|
||||
"position_advice": "仓位建议(如:谨慎/标准/积极)"
|
||||
}}"""
|
||||
|
||||
llm_output = llm_call(
|
||||
messages=[
|
||||
{"role": "system", "content": self.INVESTMENT_ANALYSIS_PROMPT},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=400
|
||||
)
|
||||
|
||||
if llm_output:
|
||||
# 清理 JSON
|
||||
llm_output = llm_output.strip()
|
||||
if llm_output.startswith("```"):
|
||||
llm_output = re.sub(r'^```(?:json)?\s*', '', llm_output)
|
||||
llm_output = re.sub(r'\s*```$', '', llm_output)
|
||||
|
||||
insights = json.loads(llm_output)
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("🤖 AI 投资洞察:")
|
||||
print("=" * 50)
|
||||
print(f"📊 市场观点: {insights.get('market_view', '')}")
|
||||
print(f"⭐ 最优选择: {insights.get('top_pick', '')} - {insights.get('top_pick_reason', '')}")
|
||||
print(f"⚠️ 风险提示: {insights.get('risk_warning', '')}")
|
||||
print(f"💰 仓位建议: {insights.get('position_advice', '')}")
|
||||
print("=" * 50)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[PM] LLM 洞察生成失败: {e}")
|
||||
|
||||
def generate_trade_orders(
|
||||
self,
|
||||
trust_results: List[Dict],
|
||||
market_data: Dict[str, Dict]
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
生成交易订单
|
||||
|
||||
策略:
|
||||
1. 买入信号:Trust Index >= 60 且 verdict = "buy"
|
||||
2. 卖出信号:verdict = "sell" 或 "reject"
|
||||
3. 仓位计算:Trust Index 越高,仓位越大
|
||||
|
||||
Args:
|
||||
trust_results: batch_calculate_trust_index() 的输出
|
||||
market_data: 市场数据(用于获取价格)
|
||||
|
||||
Returns:
|
||||
订单列表
|
||||
"""
|
||||
orders = []
|
||||
|
||||
print(f"[PM] 生成交易订单...")
|
||||
|
||||
# 可用资金(假设当前空仓)
|
||||
available_capital = self._calculate_available_capital()
|
||||
|
||||
for trust_result in trust_results:
|
||||
etf_code = trust_result['code']
|
||||
verdict = trust_result['verdict']
|
||||
trust_index = trust_result['trust_index']
|
||||
|
||||
# 获取当前价格
|
||||
realtime_data = market_data.get(etf_code, {}).get("realtime")
|
||||
if not realtime_data:
|
||||
continue
|
||||
|
||||
price = realtime_data['price']
|
||||
|
||||
# 判断操作
|
||||
if verdict == "buy" and trust_index >= self.min_trust_score:
|
||||
order = self._create_buy_order(
|
||||
etf_code,
|
||||
trust_index,
|
||||
price,
|
||||
available_capital
|
||||
)
|
||||
if order:
|
||||
orders.append(order)
|
||||
available_capital -= order['amount']
|
||||
|
||||
elif verdict in ["sell", "reject"] and etf_code in self.current_positions:
|
||||
order = self._create_sell_order(etf_code, price)
|
||||
if order:
|
||||
orders.append(order)
|
||||
|
||||
print(f"[PM] 生成 {len(orders)} 条订单")
|
||||
return orders
|
||||
|
||||
def _create_buy_order(
|
||||
self,
|
||||
etf_code: str,
|
||||
trust_index: float,
|
||||
price: float,
|
||||
available_capital: float
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
创建买入订单
|
||||
|
||||
仓位计算:
|
||||
position_pct = min(trust_index / 100 * max_position_pct, max_position_pct)
|
||||
"""
|
||||
# 已持仓则跳过
|
||||
if etf_code in self.current_positions:
|
||||
return None
|
||||
|
||||
# 计算目标仓位
|
||||
position_pct = min(
|
||||
(trust_index / 100) * self.max_position_pct,
|
||||
self.max_position_pct
|
||||
)
|
||||
|
||||
target_amount = self.total_capital * position_pct
|
||||
|
||||
# 检查可用资金
|
||||
if target_amount > available_capital:
|
||||
target_amount = available_capital * 0.8 # 保留20%缓冲
|
||||
|
||||
if target_amount < 1000: # 最小交易金额
|
||||
return None
|
||||
|
||||
shares = int(target_amount / price / 100) * 100 # 向下取整到100股
|
||||
actual_amount = shares * price
|
||||
|
||||
return {
|
||||
"action": "buy",
|
||||
"code": etf_code,
|
||||
"price": price,
|
||||
"shares": shares,
|
||||
"amount": round(actual_amount, 2),
|
||||
"position_pct": round(actual_amount / self.total_capital * 100, 2),
|
||||
"trust_index": trust_index,
|
||||
"reason": f"Trust Index {trust_index:.1f},叙事与资金共振",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def _create_sell_order(
|
||||
self,
|
||||
etf_code: str,
|
||||
price: float
|
||||
) -> Optional[Dict]:
|
||||
"""创建卖出订单"""
|
||||
if etf_code not in self.current_positions:
|
||||
return None
|
||||
|
||||
position = self.current_positions[etf_code]
|
||||
shares = position['shares']
|
||||
amount = shares * price
|
||||
|
||||
return {
|
||||
"action": "sell",
|
||||
"code": etf_code,
|
||||
"price": price,
|
||||
"shares": shares,
|
||||
"amount": round(amount, 2),
|
||||
"reason": "Trust Index 下降或一票否决触发",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def _calculate_available_capital(self) -> float:
|
||||
"""计算可用资金"""
|
||||
# 简化版:假设当前空仓
|
||||
# 实际应该是:总资金 - 已用资金
|
||||
return self.total_capital
|
||||
|
||||
def generate_portfolio_report(
|
||||
self,
|
||||
trust_results: List[Dict],
|
||||
orders: List[Dict]
|
||||
) -> Dict:
|
||||
"""
|
||||
生成投资组合报告
|
||||
|
||||
Args:
|
||||
trust_results: Trust Index 结果
|
||||
orders: 交易订单
|
||||
|
||||
Returns:
|
||||
报告字典
|
||||
"""
|
||||
# 统计 verdict 分布
|
||||
verdict_counts = {}
|
||||
for result in trust_results:
|
||||
verdict = result['verdict']
|
||||
verdict_counts[verdict] = verdict_counts.get(verdict, 0) + 1
|
||||
|
||||
# 统计订单金额
|
||||
total_buy_amount = sum(
|
||||
order['amount'] for order in orders if order['action'] == 'buy'
|
||||
)
|
||||
total_sell_amount = sum(
|
||||
order['amount'] for order in orders if order['action'] == 'sell'
|
||||
)
|
||||
|
||||
# Top 5 高信任度 ETF
|
||||
top_5 = trust_results[:5]
|
||||
|
||||
report = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"total_capital": self.total_capital,
|
||||
"analysis": {
|
||||
"total_etfs": len(trust_results),
|
||||
"verdict_distribution": verdict_counts,
|
||||
"avg_trust_index": round(
|
||||
sum(r['trust_index'] for r in trust_results) / len(trust_results), 2
|
||||
) if trust_results else 0
|
||||
},
|
||||
"orders": {
|
||||
"total_orders": len(orders),
|
||||
"buy_orders": len([o for o in orders if o['action'] == 'buy']),
|
||||
"sell_orders": len([o for o in orders if o['action'] == 'sell']),
|
||||
"total_buy_amount": round(total_buy_amount, 2),
|
||||
"total_sell_amount": round(total_sell_amount, 2)
|
||||
},
|
||||
"top_opportunities": [
|
||||
{
|
||||
"code": r['code'],
|
||||
"trust_index": r['trust_index'],
|
||||
"verdict": r['verdict']
|
||||
}
|
||||
for r in top_5
|
||||
],
|
||||
"risk_control": {
|
||||
"capital_utilization": round(total_buy_amount / self.total_capital * 100, 2),
|
||||
"max_single_position": self.max_position_pct * 100,
|
||||
"min_trust_threshold": self.min_trust_score
|
||||
}
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
def apply_risk_control(
|
||||
self,
|
||||
orders: List[Dict]
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
风控检查
|
||||
|
||||
规则:
|
||||
1. 单个 ETF 不超过最大仓位
|
||||
2. 总仓位不超过90%(保留10%现金)
|
||||
3. 同板块 ETF 合计不超过30%
|
||||
|
||||
Args:
|
||||
orders: 原始订单列表
|
||||
|
||||
Returns:
|
||||
风控后的订单列表
|
||||
"""
|
||||
filtered_orders = []
|
||||
total_amount = 0
|
||||
|
||||
for order in orders:
|
||||
if order['action'] == 'buy':
|
||||
# 检查单仓位
|
||||
if order['position_pct'] > self.max_position_pct * 100:
|
||||
print(f"[PM] 风控拒绝: {order['code']} 仓位过大")
|
||||
continue
|
||||
|
||||
# 检查总仓位
|
||||
new_total = total_amount + order['amount']
|
||||
if new_total / self.total_capital > 0.9:
|
||||
print(f"[PM] 风控拒绝: {order['code']} 总仓位将超90%")
|
||||
continue
|
||||
|
||||
total_amount = new_total
|
||||
|
||||
filtered_orders.append(order)
|
||||
|
||||
print(f"[PM] 风控完成,通过 {len(filtered_orders)}/{len(orders)} 条订单")
|
||||
return filtered_orders
|
||||
|
||||
|
||||
# ==================== 工具函数 ====================
|
||||
|
||||
def save_orders_to_file(orders: List[Dict], filename: str = "trade_orders.json") -> None:
|
||||
"""保存订单到文件"""
|
||||
output_path = Path("data") / filename
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"orders": orders
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"[PM] 订单已保存到 {output_path}")
|
||||
|
||||
|
||||
# ==================== 测试代码 ====================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Skill D: PM 基金经理测试")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建 WorldBook 和 PM
|
||||
wb = WorldBook(data_dir="data")
|
||||
pm = PortfolioManager(wb, total_capital=1000000)
|
||||
|
||||
# 模拟数据:添加测试叙事
|
||||
from core.world_book import Narrative, create_narrative_id
|
||||
|
||||
narrative1 = Narrative(
|
||||
id=create_narrative_id("AI算力"),
|
||||
topic="AI算力",
|
||||
related_etfs=["515980"],
|
||||
lifecycle_stage="realization",
|
||||
base_score=88.0,
|
||||
current_weight=88.0
|
||||
)
|
||||
wb.add_narrative(narrative1)
|
||||
|
||||
# 模拟 Quant 输出的市场数据
|
||||
mock_market_data = {
|
||||
"515980": {
|
||||
"flow_analysis": {"flow_score": 72.5},
|
||||
"realtime": {"price": 1.25, "name": "AI算力ETF"}
|
||||
},
|
||||
"512480": {
|
||||
"flow_analysis": {"flow_score": 45.0},
|
||||
"realtime": {"price": 2.10, "name": "半导体ETF"}
|
||||
}
|
||||
}
|
||||
|
||||
# 1. 批量计算 Trust Index
|
||||
print("\n1. 计算 Trust Index:")
|
||||
trust_results = pm.batch_calculate_trust_index(mock_market_data)
|
||||
|
||||
for result in trust_results:
|
||||
print(f" {result['code']}: Trust={result['trust_index']}, "
|
||||
f"叙事={result['narrative_score']}, 资金={result['flow_score']}, "
|
||||
f"判定={result['verdict']}")
|
||||
|
||||
# 2. 生成交易订单
|
||||
print("\n2. 生成交易订单:")
|
||||
orders = pm.generate_trade_orders(trust_results, mock_market_data)
|
||||
|
||||
for order in orders:
|
||||
print(f" {order['action'].upper()} {order['code']}: "
|
||||
f"{order['shares']}股 @ ¥{order['price']}, "
|
||||
f"金额 ¥{order['amount']:,.0f}")
|
||||
|
||||
# 3. 风控检查
|
||||
print("\n3. 风控检查:")
|
||||
safe_orders = pm.apply_risk_control(orders)
|
||||
|
||||
# 4. 生成投资组合报告
|
||||
print("\n4. 投资组合报告:")
|
||||
report = pm.generate_portfolio_report(trust_results, safe_orders)
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
|
||||
# 5. 保存订单
|
||||
if safe_orders:
|
||||
save_orders_to_file(safe_orders)
|
||||
|
||||
print("\n✅ PM 模块测试完成")
|
||||
406
skills/quant.py
Normal file
406
skills/quant.py
Normal file
@ -0,0 +1,406 @@
|
||||
"""
|
||||
Skill B: Quant (量化研究员)
|
||||
============================
|
||||
职能:行情分析、资金流计算、异常检测
|
||||
|
||||
输入:ETF 代码列表
|
||||
输出:Market_Data_JSON (资金流评分、技术指标、异常信号)
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from core.config import config, llm_call
|
||||
import akshare as ak
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
|
||||
class QuantAnalyzer:
|
||||
"""量化分析器 - 负责 ETF 行情数据获取和资金流分析"""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {} # 简单缓存机制
|
||||
|
||||
def get_etf_realtime_data(self, etf_code: str) -> Optional[Dict]:
|
||||
"""
|
||||
获取 ETF 实时行情数据
|
||||
|
||||
Args:
|
||||
etf_code: ETF 代码(如 "512980")
|
||||
|
||||
Returns:
|
||||
实时行情字典
|
||||
"""
|
||||
try:
|
||||
# 使用 akshare 获取实时行情
|
||||
df = ak.fund_etf_spot_em()
|
||||
|
||||
# 查找目标 ETF
|
||||
etf_data = df[df['代码'] == etf_code]
|
||||
|
||||
if etf_data.empty:
|
||||
print(f"[Quant] 未找到 ETF: {etf_code}")
|
||||
return None
|
||||
|
||||
row = etf_data.iloc[0]
|
||||
|
||||
return {
|
||||
"code": etf_code,
|
||||
"name": row['名称'],
|
||||
"price": float(row['最新价']),
|
||||
"change_pct": float(row['涨跌幅']),
|
||||
"volume": float(row['成交量']),
|
||||
"amount": float(row['成交额']),
|
||||
"turnover_rate": float(row.get('换手率', 0)),
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Quant] 获取实时数据失败 {etf_code}: {e}")
|
||||
return None
|
||||
|
||||
def get_etf_historical_data(
|
||||
self,
|
||||
etf_code: str,
|
||||
days: int = 20
|
||||
) -> Optional[pd.DataFrame]:
|
||||
"""
|
||||
获取 ETF 历史行情数据
|
||||
|
||||
Args:
|
||||
etf_code: ETF 代码
|
||||
days: 获取天数(默认20个交易日)
|
||||
|
||||
Returns:
|
||||
历史行情 DataFrame
|
||||
"""
|
||||
try:
|
||||
# 计算日期范围
|
||||
end_date = datetime.now().strftime("%Y%m%d")
|
||||
start_date = (datetime.now() - timedelta(days=days*2)).strftime("%Y%m%d")
|
||||
|
||||
# 获取历史数据
|
||||
df = ak.fund_etf_hist_em(
|
||||
symbol=etf_code,
|
||||
period="daily",
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
adjust="qfq"
|
||||
)
|
||||
|
||||
if df is None or df.empty:
|
||||
print(f"[Quant] 无历史数据: {etf_code}")
|
||||
return None
|
||||
|
||||
# 重命名列
|
||||
df.rename(columns={
|
||||
'日期': 'date',
|
||||
'开盘': 'open',
|
||||
'收盘': 'close',
|
||||
'最高': 'high',
|
||||
'最低': 'low',
|
||||
'成交量': 'volume',
|
||||
'成交额': 'amount',
|
||||
'涨跌幅': 'change_pct',
|
||||
'换手率': 'turnover_rate'
|
||||
}, inplace=True)
|
||||
|
||||
return df.tail(days)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Quant] 获取历史数据失败 {etf_code}: {e}")
|
||||
return None
|
||||
|
||||
def calculate_fund_flow_score(
|
||||
self,
|
||||
etf_code: str,
|
||||
window: int = 5
|
||||
) -> Dict:
|
||||
"""
|
||||
计算资金流向评分
|
||||
|
||||
核心逻辑:
|
||||
1. 成交额放量 (amount_surge)
|
||||
2. 换手率提升 (turnover_boost)
|
||||
3. 价格与成交量背离检测
|
||||
|
||||
Args:
|
||||
etf_code: ETF 代码
|
||||
window: 计算窗口(天数)
|
||||
|
||||
Returns:
|
||||
资金流评分字典
|
||||
"""
|
||||
df = self.get_etf_historical_data(etf_code, days=window*2)
|
||||
|
||||
if df is None or len(df) < window:
|
||||
return {
|
||||
"code": etf_code,
|
||||
"flow_score": 0,
|
||||
"amount_surge": 0,
|
||||
"turnover_boost": 0,
|
||||
"divergence_signal": False,
|
||||
"status": "insufficient_data"
|
||||
}
|
||||
|
||||
# 近期数据 vs 历史基准
|
||||
recent = df.tail(window)
|
||||
baseline = df.head(window)
|
||||
|
||||
# 1. 成交额放量评分 (0-50)
|
||||
recent_avg_amount = recent['amount'].mean()
|
||||
baseline_avg_amount = baseline['amount'].mean()
|
||||
|
||||
if baseline_avg_amount > 0:
|
||||
amount_ratio = recent_avg_amount / baseline_avg_amount
|
||||
amount_surge_score = min((amount_ratio - 1) * 50, 50)
|
||||
else:
|
||||
amount_surge_score = 0
|
||||
|
||||
# 2. 换手率提升评分 (0-30)
|
||||
recent_avg_turnover = recent['turnover_rate'].mean()
|
||||
baseline_avg_turnover = baseline['turnover_rate'].mean()
|
||||
|
||||
if baseline_avg_turnover > 0:
|
||||
turnover_ratio = recent_avg_turnover / baseline_avg_turnover
|
||||
turnover_boost_score = min((turnover_ratio - 1) * 30, 30)
|
||||
else:
|
||||
turnover_boost_score = 0
|
||||
|
||||
# 3. 价格与成交量背离检测 (奖励分 +20)
|
||||
recent_price_change = recent['close'].iloc[-1] / recent['close'].iloc[0] - 1
|
||||
recent_volume_change = recent['volume'].iloc[-1] / recent['volume'].iloc[0] - 1
|
||||
|
||||
# 价跌量增 = 可能见底信号
|
||||
divergence_signal = (recent_price_change < -0.02 and recent_volume_change > 0.3)
|
||||
divergence_bonus = 20 if divergence_signal else 0
|
||||
|
||||
# 总分计算 (0-100)
|
||||
total_score = max(0, min(100,
|
||||
amount_surge_score + turnover_boost_score + divergence_bonus
|
||||
))
|
||||
|
||||
return {
|
||||
"code": etf_code,
|
||||
"flow_score": round(total_score, 2),
|
||||
"amount_surge": round(amount_surge_score, 2),
|
||||
"turnover_boost": round(turnover_boost_score, 2),
|
||||
"divergence_signal": divergence_signal,
|
||||
"recent_avg_amount": round(recent_avg_amount / 1e8, 2), # 单位:亿
|
||||
"baseline_avg_amount": round(baseline_avg_amount / 1e8, 2),
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
def detect_anomaly(
|
||||
self,
|
||||
etf_code: str,
|
||||
sigma: float = 2.0
|
||||
) -> Dict:
|
||||
"""
|
||||
异常检测 - 识别异常放量或暴涨暴跌
|
||||
|
||||
Args:
|
||||
etf_code: ETF 代码
|
||||
sigma: 标准差倍数(默认2倍)
|
||||
|
||||
Returns:
|
||||
异常信号字典
|
||||
"""
|
||||
df = self.get_etf_historical_data(etf_code, days=30)
|
||||
|
||||
if df is None or len(df) < 10:
|
||||
return {"code": etf_code, "has_anomaly": False, "reason": "insufficient_data"}
|
||||
|
||||
# 计算统计基准
|
||||
mean_amount = df['amount'].mean()
|
||||
std_amount = df['amount'].std()
|
||||
mean_change = df['change_pct'].mean()
|
||||
std_change = df['change_pct'].std()
|
||||
|
||||
# 最新数据
|
||||
latest = df.iloc[-1]
|
||||
|
||||
anomaly_signals = []
|
||||
|
||||
# 成交额异常
|
||||
if latest['amount'] > mean_amount + sigma * std_amount:
|
||||
anomaly_signals.append("volume_surge")
|
||||
|
||||
# 涨跌幅异常
|
||||
if abs(latest['change_pct']) > abs(mean_change) + sigma * std_change:
|
||||
if latest['change_pct'] > 0:
|
||||
anomaly_signals.append("price_spike")
|
||||
else:
|
||||
anomaly_signals.append("price_crash")
|
||||
|
||||
return {
|
||||
"code": etf_code,
|
||||
"has_anomaly": len(anomaly_signals) > 0,
|
||||
"signals": anomaly_signals,
|
||||
"latest_amount": round(latest['amount'] / 1e8, 2),
|
||||
"latest_change_pct": round(latest['change_pct'], 2),
|
||||
"threshold_amount": round((mean_amount + sigma * std_amount) / 1e8, 2),
|
||||
"threshold_change": round(abs(mean_change) + sigma * std_change, 2)
|
||||
}
|
||||
|
||||
def batch_analyze(
|
||||
self,
|
||||
etf_codes: List[str]
|
||||
) -> Dict[str, Dict]:
|
||||
"""
|
||||
批量分析 ETF
|
||||
|
||||
Args:
|
||||
etf_codes: ETF 代码列表
|
||||
|
||||
Returns:
|
||||
分析结果字典 {code: result}
|
||||
"""
|
||||
results = {}
|
||||
|
||||
print(f"[Quant] 开始批量分析 {len(etf_codes)} 个 ETF...")
|
||||
|
||||
for code in etf_codes:
|
||||
print(f"[Quant] 分析 {code}...")
|
||||
|
||||
# 资金流评分
|
||||
flow_result = self.calculate_fund_flow_score(code)
|
||||
|
||||
# 异常检测
|
||||
anomaly_result = self.detect_anomaly(code)
|
||||
|
||||
# 实时行情
|
||||
realtime_data = self.get_etf_realtime_data(code)
|
||||
|
||||
results[code] = {
|
||||
"flow_analysis": flow_result,
|
||||
"anomaly_detection": anomaly_result,
|
||||
"realtime": realtime_data,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
print(f"[Quant] 批量分析完成")
|
||||
return results
|
||||
|
||||
def generate_market_report(
|
||||
self,
|
||||
analysis_results: Dict[str, Dict]
|
||||
) -> Dict:
|
||||
"""
|
||||
生成市场数据报告(供其他 Agent 使用)
|
||||
|
||||
Args:
|
||||
analysis_results: batch_analyze 的输出
|
||||
|
||||
Returns:
|
||||
标准化的 Market_Data_JSON
|
||||
"""
|
||||
report = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"total_analyzed": len(analysis_results),
|
||||
"etf_scores": {},
|
||||
"top_flow_etfs": [],
|
||||
"anomaly_alerts": []
|
||||
}
|
||||
|
||||
# 提取评分
|
||||
for code, result in analysis_results.items():
|
||||
flow_score = result.get("flow_analysis", {}).get("flow_score", 0)
|
||||
report["etf_scores"][code] = flow_score
|
||||
|
||||
# 异常提醒
|
||||
if result.get("anomaly_detection", {}).get("has_anomaly"):
|
||||
report["anomaly_alerts"].append({
|
||||
"code": code,
|
||||
"signals": result["anomaly_detection"]["signals"]
|
||||
})
|
||||
|
||||
# 排序 Top 资金流 ETF
|
||||
sorted_etfs = sorted(
|
||||
report["etf_scores"].items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)
|
||||
report["top_flow_etfs"] = [
|
||||
{"code": code, "score": score}
|
||||
for code, score in sorted_etfs[:5]
|
||||
]
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# ==================== 工具函数 ====================
|
||||
|
||||
def get_all_etf_codes_from_asset_map() -> List[str]:
|
||||
"""从 asset_map.json 提取所有 ETF 代码"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
asset_map_path = Path("core") / "asset_map.json"
|
||||
|
||||
if not asset_map_path.exists():
|
||||
print("[Quant] 未找到 asset_map.json")
|
||||
return []
|
||||
|
||||
with open(asset_map_path, 'r', encoding='utf-8') as f:
|
||||
asset_map = json.load(f)
|
||||
|
||||
etf_codes = set()
|
||||
for sector_data in asset_map.get("sectors", {}).values():
|
||||
etf_codes.update(sector_data.get("etfs", []))
|
||||
|
||||
return list(etf_codes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Skill B: Quant 量化研究员测试")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建分析器实例
|
||||
quant = QuantAnalyzer()
|
||||
|
||||
# 测试单个 ETF
|
||||
test_code = "512480" # 半导体 ETF
|
||||
print(f"\n测试 ETF: {test_code}")
|
||||
|
||||
# 1. 获取实时数据
|
||||
print("\n1. 实时行情:")
|
||||
realtime = quant.get_etf_realtime_data(test_code)
|
||||
if realtime:
|
||||
print(f" {realtime['name']}: ¥{realtime['price']}, 涨跌幅: {realtime['change_pct']}%")
|
||||
|
||||
# 2. 资金流评分
|
||||
print("\n2. 资金流分析:")
|
||||
flow = quant.calculate_fund_flow_score(test_code)
|
||||
print(f" 资金流评分: {flow['flow_score']}")
|
||||
print(f" 成交额放量: {flow['amount_surge']}")
|
||||
print(f" 换手率提升: {flow['turnover_boost']}")
|
||||
|
||||
# 3. 异常检测
|
||||
print("\n3. 异常检测:")
|
||||
anomaly = quant.detect_anomaly(test_code)
|
||||
print(f" 发现异常: {anomaly['has_anomaly']}")
|
||||
if anomaly['has_anomaly']:
|
||||
print(f" 异常信号: {anomaly['signals']}")
|
||||
|
||||
# 4. 批量分析(测试少量 ETF)
|
||||
print("\n4. 批量分析测试:")
|
||||
test_codes = ["512480", "515980", "159928"] # 半导体、AI、消费
|
||||
batch_results = quant.batch_analyze(test_codes)
|
||||
|
||||
# 5. 生成报告
|
||||
report = quant.generate_market_report(batch_results)
|
||||
print(f"\n5. 市场报告:")
|
||||
print(f" 分析数量: {report['total_analyzed']}")
|
||||
print(f" Top 资金流 ETF:")
|
||||
for item in report['top_flow_etfs']:
|
||||
print(f" {item['code']}: {item['score']:.2f}")
|
||||
|
||||
print("\n✅ Quant 模块测试完成")
|
||||
742
skills/strategist.py
Normal file
742
skills/strategist.py
Normal file
@ -0,0 +1,742 @@
|
||||
"""
|
||||
Skill C: Strategist (宏观策略师)
|
||||
=================================
|
||||
职能:维护世界书、判断周期、更新叙事权重
|
||||
|
||||
输入:Narrative_JSON + 历史 WorldBook
|
||||
输出:更新后的 World_Book
|
||||
|
||||
设计原则:
|
||||
- 使用 LLM 进行宏观周期判断和叙事生命周期分析
|
||||
- 结合规则引擎提供可解释的决策逻辑
|
||||
- 支持 LLM 失败时的优雅降级
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from core.world_book import WorldBook, Narrative, create_narrative_id
|
||||
from core.config import config, llm_call
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
|
||||
class MacroStrategist:
|
||||
"""
|
||||
宏观策略师 - 负责 World Book 的智能维护
|
||||
|
||||
核心职责:
|
||||
1. 接收 Analyst 的新闻分析结果
|
||||
2. 使用 LLM 进行宏观周期和叙事生命周期判断
|
||||
3. 智能更新或创建叙事对象
|
||||
4. 应用时间衰减
|
||||
5. 生成投资策略建议
|
||||
"""
|
||||
|
||||
# LLM 系统提示词
|
||||
MACRO_ANALYSIS_PROMPT = """你是一位资深的 A 股宏观策略分析师,擅长根据政策新闻判断市场周期变化。
|
||||
|
||||
你的核心任务是:
|
||||
1. 分析当前新闻叙事对宏观环境的影响
|
||||
2. 判断市场所处的周期阶段
|
||||
3. 识别关键的宏观驱动因子
|
||||
4. 给出策略配置建议
|
||||
|
||||
宏观周期状态:
|
||||
- upward(上行):政策利好、流动性充裕、风险偏好上升
|
||||
- downward(下行):政策收紧、流动性紧张、风险偏好下降
|
||||
- neutral(中性):政策观望、流动性平稳
|
||||
|
||||
流动性状态:
|
||||
- loose(宽松):降准降息、央行投放
|
||||
- tight(紧张):加息、回笼资金
|
||||
- neutral(中性)
|
||||
|
||||
政策风向:
|
||||
- stimulus(刺激):财政扩张、产业扶持
|
||||
- regulation(监管):整顿规范、去杠杆
|
||||
- wait_and_see(观望)
|
||||
|
||||
请严格按照 JSON 格式返回分析结果。"""
|
||||
|
||||
def __init__(self, world_book: WorldBook, use_llm: bool = True):
|
||||
"""
|
||||
Args:
|
||||
world_book: WorldBook 实例
|
||||
use_llm: 是否使用 LLM 进行分析(默认 True)
|
||||
"""
|
||||
self.wb = world_book
|
||||
self.macro_matrix = self._load_macro_matrix()
|
||||
self.use_llm = use_llm
|
||||
|
||||
if self.use_llm:
|
||||
print("[Strategist] ✅ LLM 模式启用 - 将使用大模型进行智能策略分析")
|
||||
else:
|
||||
print("[Strategist] ⚠️ 降级模式 - 将使用规则引擎")
|
||||
|
||||
def _load_macro_matrix(self) -> Dict:
|
||||
"""加载宏观逻辑矩阵"""
|
||||
matrix_path = Path("core") / "macro_matrix.json"
|
||||
if not matrix_path.exists():
|
||||
print("[Strategist] 警告: 未找到 macro_matrix.json")
|
||||
return {}
|
||||
|
||||
with open(matrix_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def process_narrative_json(
|
||||
self,
|
||||
narrative_json: Dict
|
||||
) -> None:
|
||||
"""
|
||||
处理 Analyst 输出的 Narrative_JSON
|
||||
|
||||
逻辑:
|
||||
1. 遍历所有叙事
|
||||
2. 如果是新叙事 → 创建
|
||||
3. 如果已存在 → 强化(boost)
|
||||
4. 更新生命周期阶段
|
||||
|
||||
Args:
|
||||
narrative_json: Analyst.generate_narrative_json() 的输出
|
||||
"""
|
||||
narratives = narrative_json.get('narratives', [])
|
||||
print(f"[Strategist] 处理 {len(narratives)} 个叙事...")
|
||||
|
||||
for narrative_data in narratives:
|
||||
topic = narrative_data['topic']
|
||||
|
||||
# 检查是否已存在
|
||||
existing_narrative = self.wb.get_narrative_by_topic(topic)
|
||||
|
||||
if existing_narrative:
|
||||
# 已存在 → 强化
|
||||
self._boost_narrative(existing_narrative, narrative_data)
|
||||
else:
|
||||
# 新叙事 → 创建
|
||||
self._create_narrative(narrative_data)
|
||||
|
||||
# 推导宏观因子向量
|
||||
self.infer_macro_factor_vector(narrative_json)
|
||||
|
||||
print(f"[Strategist] 叙事处理完成,当前活跃叙事数: {len(self.wb.active_narratives)}")
|
||||
|
||||
def _create_narrative(self, narrative_data: Dict) -> None:
|
||||
"""创建新叙事对象"""
|
||||
topic = narrative_data['topic']
|
||||
|
||||
# 生成唯一 ID
|
||||
narrative_id = create_narrative_id(topic)
|
||||
|
||||
# 判断衰减系数(Level A 新闻衰减慢)
|
||||
decay_factor = 0.93 if narrative_data['level_a_count'] > 0 else 0.95
|
||||
|
||||
# 创建 Narrative 对象
|
||||
narrative = Narrative(
|
||||
id=narrative_id,
|
||||
topic=topic,
|
||||
related_etfs=narrative_data['related_etfs'],
|
||||
lifecycle_stage=narrative_data['lifecycle_stage'],
|
||||
base_score=narrative_data['max_score'],
|
||||
decay_factor=decay_factor,
|
||||
current_weight=narrative_data['avg_score']
|
||||
)
|
||||
|
||||
self.wb.add_narrative(narrative)
|
||||
print(f"[Strategist] 创建新叙事: {topic} (评分: {narrative.current_weight})")
|
||||
|
||||
def _boost_narrative(
|
||||
self,
|
||||
existing_narrative: Narrative,
|
||||
narrative_data: Dict
|
||||
) -> None:
|
||||
"""强化已有叙事"""
|
||||
topic = narrative_data['topic']
|
||||
new_score = narrative_data['avg_score']
|
||||
|
||||
# 使用 Narrative 的 boost 方法
|
||||
existing_narrative.boost(new_score)
|
||||
|
||||
# 如果有 Level A 新闻,可能需要升级生命周期
|
||||
if narrative_data['level_a_count'] > 0:
|
||||
if existing_narrative.lifecycle_stage == "fermentation":
|
||||
existing_narrative.update_stage("realization")
|
||||
|
||||
print(f"[Strategist] 强化叙事: {topic} → {existing_narrative.current_weight:.2f}")
|
||||
|
||||
def daily_maintenance(self) -> Dict:
|
||||
"""
|
||||
每日维护任务
|
||||
|
||||
执行:
|
||||
1. 对所有叙事应用时间衰减
|
||||
2. 移除衰退的叙事
|
||||
3. 检测生命周期阶段变化
|
||||
|
||||
Returns:
|
||||
维护报告
|
||||
"""
|
||||
print("[Strategist] 执行每日维护...")
|
||||
|
||||
# 1. 应用衰减
|
||||
self.wb.decay_all_narratives()
|
||||
|
||||
# 2. 检测阶段降级
|
||||
for narrative in self.wb.active_narratives.values():
|
||||
self._check_stage_downgrade(narrative)
|
||||
|
||||
# 3. 移除低权重叙事
|
||||
removed = self.wb.remove_weak_narratives(threshold=10.0)
|
||||
|
||||
# 4. 生成报告
|
||||
report = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"total_narratives": len(self.wb.active_narratives),
|
||||
"removed_narratives": len(removed),
|
||||
"top_narratives": [
|
||||
{"topic": n.topic, "weight": n.current_weight}
|
||||
for n in self.wb.get_top_narratives(5)
|
||||
]
|
||||
}
|
||||
|
||||
print(f"[Strategist] 维护完成,移除 {len(removed)} 个衰退叙事")
|
||||
return report
|
||||
|
||||
def _check_stage_downgrade(self, narrative: Narrative) -> None:
|
||||
"""检测叙事是否需要降级生命周期"""
|
||||
current_stage = narrative.lifecycle_stage
|
||||
weight = narrative.current_weight
|
||||
|
||||
# 权重低于阈值 → 进入衰退期
|
||||
if weight < 30 and current_stage != "decay":
|
||||
narrative.update_stage("decay")
|
||||
print(f"[Strategist] 叙事进入衰退期: {narrative.topic}")
|
||||
|
||||
# realization → fermentation
|
||||
elif weight < 60 and current_stage == "realization":
|
||||
narrative.update_stage("fermentation")
|
||||
|
||||
def detect_macro_cycle_change(
|
||||
self,
|
||||
narrative_json: Dict,
|
||||
market_data: Optional[Dict] = None
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
检测宏观周期变化(核心方法)
|
||||
|
||||
优先使用 LLM 进行智能分析,失败时降级到规则引擎
|
||||
|
||||
Args:
|
||||
narrative_json: Analyst 输出
|
||||
market_data: Quant 输出(可选)
|
||||
|
||||
Returns:
|
||||
宏观周期变化建议(如有)
|
||||
"""
|
||||
# 检查是否有 Level A 新闻
|
||||
level_a_count = sum(
|
||||
n.get('level_a_count', 0)
|
||||
for n in narrative_json.get('narratives', [])
|
||||
)
|
||||
|
||||
if level_a_count == 0:
|
||||
print("[Strategist] 无 Level A 新闻,跳过宏观周期检测")
|
||||
return None
|
||||
|
||||
print(f"[Strategist] 检测到 {level_a_count} 条 Level A 新闻,分析宏观周期...")
|
||||
|
||||
if self.use_llm:
|
||||
result = self._detect_cycle_with_llm(narrative_json, market_data)
|
||||
if result:
|
||||
return result
|
||||
print("[Strategist] ⚠️ LLM 分析失败,降级到规则引擎")
|
||||
|
||||
# 降级到规则引擎
|
||||
return self._detect_cycle_with_rules(narrative_json)
|
||||
|
||||
def _detect_cycle_with_llm(
|
||||
self,
|
||||
narrative_json: Dict,
|
||||
market_data: Optional[Dict]
|
||||
) -> Optional[Dict]:
|
||||
"""使用 LLM 分析宏观周期变化"""
|
||||
# 构建分析上下文
|
||||
narratives_summary = []
|
||||
for n in narrative_json.get('narratives', []):
|
||||
topic_name = n.get('topic_name', n.get('topic', ''))
|
||||
narratives_summary.append({
|
||||
"topic": topic_name,
|
||||
"score": n.get('max_score', 0),
|
||||
"level_a": n.get('level_a_count', 0) > 0,
|
||||
"sentiment": n.get('overall_sentiment', 'neutral'),
|
||||
"signals": n.get('key_signals', [])[:2]
|
||||
})
|
||||
|
||||
# 当前宏观状态
|
||||
current_macro = self.wb.macro_cycle.to_dict()
|
||||
|
||||
prompt = f"""请分析以下市场叙事信息,判断宏观周期是否发生变化:
|
||||
|
||||
【当前宏观状态】
|
||||
- 市场周期: {current_macro['status']}
|
||||
- 流动性: {current_macro['liquidity']}
|
||||
- 政策风向: {current_macro['policy_wind']}
|
||||
|
||||
【最新叙事信息】
|
||||
{json.dumps(narratives_summary, ensure_ascii=False, indent=2)}
|
||||
|
||||
请分析后以 JSON 格式返回:
|
||||
{{
|
||||
"cycle_changed": true/false,
|
||||
"new_status": "upward/downward/neutral",
|
||||
"new_liquidity": "loose/tight/neutral",
|
||||
"new_policy_wind": "stimulus/regulation/wait_and_see",
|
||||
"reason": "变化原因(50字内)",
|
||||
"confidence": 0-100,
|
||||
"key_factors": ["驱动因子1", "驱动因子2"]
|
||||
}}
|
||||
|
||||
如果宏观环境未发生明显变化,cycle_changed 设为 false。"""
|
||||
|
||||
try:
|
||||
llm_output = llm_call(
|
||||
messages=[
|
||||
{"role": "system", "content": self.MACRO_ANALYSIS_PROMPT},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=500
|
||||
)
|
||||
|
||||
if not llm_output:
|
||||
return None
|
||||
|
||||
# 清理并解析 JSON
|
||||
import re
|
||||
llm_output = llm_output.strip()
|
||||
if llm_output.startswith("```"):
|
||||
llm_output = re.sub(r'^```(?:json)?\s*', '', llm_output)
|
||||
llm_output = re.sub(r'\s*```$', '', llm_output)
|
||||
|
||||
result = json.loads(llm_output)
|
||||
|
||||
if not result.get("cycle_changed", False):
|
||||
print(f"[Strategist] 🤖 LLM 判断:宏观周期无明显变化")
|
||||
return None
|
||||
|
||||
# 验证字段
|
||||
valid_status = ["upward", "downward", "neutral"]
|
||||
valid_liquidity = ["loose", "tight", "neutral"]
|
||||
valid_policy = ["stimulus", "regulation", "wait_and_see"]
|
||||
|
||||
new_status = result.get("new_status", "neutral")
|
||||
new_liquidity = result.get("new_liquidity", "neutral")
|
||||
new_policy = result.get("new_policy_wind", "wait_and_see")
|
||||
|
||||
if new_status not in valid_status:
|
||||
new_status = "neutral"
|
||||
if new_liquidity not in valid_liquidity:
|
||||
new_liquidity = "neutral"
|
||||
if new_policy not in valid_policy:
|
||||
new_policy = "wait_and_see"
|
||||
|
||||
print(f"[Strategist] 🤖 LLM 判断:{result.get('reason', '')}")
|
||||
print(f"[Strategist] 🤖 置信度: {result.get('confidence', 0)}%")
|
||||
|
||||
return {
|
||||
"status": new_status,
|
||||
"liquidity": new_liquidity,
|
||||
"policy_wind": new_policy,
|
||||
"reason": result.get("reason", "LLM 分析结果"),
|
||||
"key_factors": result.get("key_factors", []),
|
||||
"analysis_method": "llm"
|
||||
}
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[Strategist] LLM 返回格式解析失败: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[Strategist] LLM 分析异常: {e}")
|
||||
return None
|
||||
|
||||
def _detect_cycle_with_rules(self, narrative_json: Dict) -> Optional[Dict]:
|
||||
"""规则引擎检测宏观周期变化(降级方案)"""
|
||||
# 检测关键词模式
|
||||
narratives_text = " ".join([
|
||||
n.get('topic', '') + " " + " ".join(n.get('key_signals', []))
|
||||
for n in narrative_json.get('narratives', [])
|
||||
])
|
||||
|
||||
# 货币宽松信号
|
||||
if any(kw in narratives_text for kw in ["降准", "降息", "宽松", "流动性"]):
|
||||
return {
|
||||
"status": "upward",
|
||||
"liquidity": "loose",
|
||||
"policy_wind": "stimulus",
|
||||
"reason": "货币政策宽松,市场流动性改善",
|
||||
"key_factors": ["降准/降息"],
|
||||
"analysis_method": "rules"
|
||||
}
|
||||
|
||||
# 监管收紧信号
|
||||
if any(kw in narratives_text for kw in ["监管", "整顿", "规范", "收紧"]):
|
||||
return {
|
||||
"status": "downward",
|
||||
"liquidity": "tight",
|
||||
"policy_wind": "regulation",
|
||||
"reason": "监管政策收紧,风险偏好下降",
|
||||
"key_factors": ["监管收紧"],
|
||||
"analysis_method": "rules"
|
||||
}
|
||||
|
||||
# 财政刺激信号
|
||||
if any(kw in narratives_text for kw in ["刺激", "万亿", "基建", "财政"]):
|
||||
return {
|
||||
"status": "upward",
|
||||
"liquidity": "loose",
|
||||
"policy_wind": "stimulus",
|
||||
"reason": "财政政策积极,经济预期改善",
|
||||
"key_factors": ["财政刺激"],
|
||||
"analysis_method": "rules"
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def apply_macro_cycle_change(self, cycle_change: Dict) -> None:
|
||||
"""应用宏观周期变化"""
|
||||
self.wb.update_macro_cycle(
|
||||
status=cycle_change.get('status'),
|
||||
liquidity=cycle_change.get('liquidity'),
|
||||
policy_wind=cycle_change.get('policy_wind')
|
||||
)
|
||||
|
||||
def infer_macro_factor_vector(self, narrative_json: Dict) -> Dict[str, float]:
|
||||
"""
|
||||
根据新闻叙事推导宏观因子向量(核心方法)
|
||||
|
||||
优先使用 LLM 进行智能推导,失败时降级到规则引擎。
|
||||
|
||||
这是核心方法,将新闻内容映射到宏观因子空间,供 PM 进行向量点积计算。
|
||||
|
||||
Args:
|
||||
narrative_json: Analyst 输出的叙事 JSON
|
||||
|
||||
Returns:
|
||||
宏观因子向量,例如:
|
||||
{
|
||||
"interest_rate_down": 1.0,
|
||||
"policy_digital_economy": 0.8,
|
||||
"geopolitics_tension": 0.5
|
||||
}
|
||||
"""
|
||||
print("[Strategist] 推导宏观因子向量...")
|
||||
|
||||
if self.use_llm:
|
||||
result = self._infer_factors_with_llm(narrative_json)
|
||||
if result:
|
||||
self.wb.update_macro_factor_vector(result)
|
||||
return result
|
||||
print("[Strategist] ⚠️ LLM 推导失败,降级到规则引擎")
|
||||
|
||||
# 降级到规则引擎
|
||||
result = self._infer_factors_with_rules(narrative_json)
|
||||
self.wb.update_macro_factor_vector(result)
|
||||
return result
|
||||
|
||||
def _infer_factors_with_llm(self, narrative_json: Dict) -> Optional[Dict[str, float]]:
|
||||
"""使用 LLM 推导宏观因子向量"""
|
||||
# 准备叙事摘要
|
||||
narratives_summary = []
|
||||
for n in narrative_json.get('narratives', []):
|
||||
narratives_summary.append({
|
||||
"topic": n.get('topic_name', n.get('topic', '')),
|
||||
"score": n.get('max_score', 0),
|
||||
"level_a": n.get('level_a_count', 0) > 0,
|
||||
"sentiment": n.get('overall_sentiment', 'neutral'),
|
||||
"key_signals": n.get('key_signals', [])
|
||||
})
|
||||
|
||||
# 可用因子列表
|
||||
available_factors = [
|
||||
"interest_rate_down", "liquidity_easing", "cpi_rebound",
|
||||
"risk_on", "risk_off", "policy_tech_self_reliance",
|
||||
"policy_digital_economy", "policy_new_infra", "policy_capital_market",
|
||||
"policy_soe_reform", "policy_low_altitude", "policy_food_security",
|
||||
"policy_public_health", "policy_platform_economy", "consumption_stimulus",
|
||||
"govt_spending", "geopolitics_tension", "currency_rmb_depreciation",
|
||||
"export_growth", "fed_rate_cut", "dollar_index_down",
|
||||
"inflation_expectations", "oil_price_up", "market_volume_spike",
|
||||
"tech_cycle_up", "demographic_trend", "foreign_inflow"
|
||||
]
|
||||
|
||||
prompt = f"""请根据以下市场叙事信息,推导当前活跃的宏观因子及其强度。
|
||||
|
||||
【叙事信息】
|
||||
{json.dumps(narratives_summary, ensure_ascii=False, indent=2)}
|
||||
|
||||
【可用宏观因子】
|
||||
{', '.join(available_factors)}
|
||||
|
||||
请分析新闻内容,判断哪些宏观因子被触发,并给出强度评分(0.0-1.0)。
|
||||
强度参考:
|
||||
- 1.0: 政策明确、信号强烈(如央行降准公告)
|
||||
- 0.7-0.9: 政策导向明确(如部委发文)
|
||||
- 0.4-0.6: 信号中等(如行业动态)
|
||||
- 0.1-0.3: 信号微弱(如市场传闻)
|
||||
|
||||
以 JSON 格式返回(仅包含被触发的因子):
|
||||
{{
|
||||
"factors": {{
|
||||
"factor_name": 0.8,
|
||||
"factor_name2": 0.5
|
||||
}},
|
||||
"reasoning": "简要说明推导逻辑(50字内)"
|
||||
}}"""
|
||||
|
||||
try:
|
||||
llm_output = llm_call(
|
||||
messages=[
|
||||
{"role": "system", "content": "你是一位宏观经济分析师,擅长从新闻中提取宏观经济信号。"},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=600
|
||||
)
|
||||
|
||||
if not llm_output:
|
||||
return None
|
||||
|
||||
# 清理并解析 JSON
|
||||
import re
|
||||
llm_output = llm_output.strip()
|
||||
if llm_output.startswith("```"):
|
||||
llm_output = re.sub(r'^```(?:json)?\s*', '', llm_output)
|
||||
llm_output = re.sub(r'\s*```$', '', llm_output)
|
||||
|
||||
result = json.loads(llm_output)
|
||||
factors = result.get("factors", {})
|
||||
reasoning = result.get("reasoning", "")
|
||||
|
||||
# 验证因子名称和强度
|
||||
valid_factors = {}
|
||||
for factor_name, strength in factors.items():
|
||||
if factor_name in available_factors:
|
||||
strength = float(strength)
|
||||
strength = max(0.0, min(1.0, strength))
|
||||
valid_factors[factor_name] = round(strength, 2)
|
||||
|
||||
if valid_factors:
|
||||
print(f"[Strategist] 🤖 LLM 推导出 {len(valid_factors)} 个宏观因子")
|
||||
print(f"[Strategist] 🤖 推导逻辑: {reasoning}")
|
||||
for factor, strength in sorted(valid_factors.items(), key=lambda x: -x[1])[:5]:
|
||||
print(f" 📊 {factor}: {strength}")
|
||||
|
||||
return valid_factors
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[Strategist] LLM 返回格式解析失败: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[Strategist] LLM 推导异常: {e}")
|
||||
return None
|
||||
|
||||
def _infer_factors_with_rules(self, narrative_json: Dict) -> Dict[str, float]:
|
||||
"""规则引擎推导宏观因子向量(降级方案)"""
|
||||
factor_vector: Dict[str, float] = {}
|
||||
|
||||
# 宏观因子关键词映射表
|
||||
factor_keyword_map = {
|
||||
"interest_rate_down": ["降息", "降准", "MLF下调", "LPR下调", "利率下行"],
|
||||
"liquidity_easing": ["流动性", "宽松", "货币政策", "注入流动性"],
|
||||
"cpi_rebound": ["CPI", "通胀", "物价上涨", "消费复苏"],
|
||||
"risk_on": ["牛市", "突破", "成交量放大", "风险偏好"],
|
||||
"risk_off": ["暴跌", "恐慌", "避险", "黑天鹅"],
|
||||
"policy_tech_self_reliance": ["自主可控", "国产替代", "卡脖子", "芯片", "半导体"],
|
||||
"policy_digital_economy": ["数字经济", "数据要素", "信创", "AI", "算力"],
|
||||
"policy_new_infra": ["新基建", "5G", "6G", "东数西算", "算力底座"],
|
||||
"policy_capital_market": ["资本市场", "印花税", "T+0", "金融强国"],
|
||||
"policy_soe_reform": ["国企改革", "中特估", "红利"],
|
||||
"policy_low_altitude": ["低空经济", "无人机", "eVTOL"],
|
||||
"policy_food_security": ["粮食安全", "一号文件", "种业"],
|
||||
"policy_public_health": ["医疗", "医保", "创新药", "老龄化"],
|
||||
"consumption_stimulus": ["消费券", "汽车下乡", "家电补贴", "内需"],
|
||||
"govt_spending": ["财政支出", "专项债", "万亿", "基建投资"],
|
||||
"geopolitics_tension": ["地缘", "制裁", "冲突", "台海", "中美"],
|
||||
"export_growth": ["出口", "外贸", "订单", "海外需求"],
|
||||
"market_volume_spike": ["成交量", "放量", "天量"],
|
||||
}
|
||||
|
||||
# 收集所有文本
|
||||
all_text = ""
|
||||
level_a_count = 0
|
||||
level_b_count = 0
|
||||
|
||||
for narrative in narrative_json.get('narratives', []):
|
||||
all_text += f" {narrative.get('topic', '')} "
|
||||
all_text += " ".join(narrative.get('key_signals', []))
|
||||
level_a_count += narrative.get('level_a_count', 0)
|
||||
level_b_count += narrative.get('level_b_count', 0)
|
||||
|
||||
# 扫描关键词
|
||||
for factor_name, keywords in factor_keyword_map.items():
|
||||
match_count = sum(1 for kw in keywords if kw in all_text)
|
||||
|
||||
if match_count > 0:
|
||||
if level_a_count > 0:
|
||||
strength = min(1.0, 0.8 + match_count * 0.1)
|
||||
elif level_b_count > 0:
|
||||
strength = min(0.9, 0.5 + match_count * 0.1)
|
||||
else:
|
||||
strength = min(0.6, 0.3 + match_count * 0.1)
|
||||
|
||||
factor_vector[factor_name] = round(strength, 2)
|
||||
|
||||
print(f"[Strategist] 📏 规则引擎推导出 {len(factor_vector)} 个宏观因子")
|
||||
return factor_vector
|
||||
|
||||
def calculate_macro_impact_on_etf(
|
||||
self,
|
||||
etf_code: str,
|
||||
macro_factor: str
|
||||
) -> float:
|
||||
"""
|
||||
计算宏观因子对特定 ETF 的影响
|
||||
|
||||
Args:
|
||||
etf_code: ETF 代码
|
||||
macro_factor: 宏观因子名称(如 "rate_cut")
|
||||
|
||||
Returns:
|
||||
影响值 (-10 到 +10)
|
||||
"""
|
||||
# 从宏观矩阵查询
|
||||
factor_data = self.macro_matrix.get("macro_factors", {}).get(macro_factor, {})
|
||||
impact_map = factor_data.get("impact", {})
|
||||
|
||||
# 查找 ETF 对应的类别
|
||||
etf_mapping = self.macro_matrix.get("etf_mapping", {})
|
||||
|
||||
for etf_type, codes in etf_mapping.items():
|
||||
if etf_code in codes:
|
||||
return impact_map.get(etf_type, 0)
|
||||
|
||||
return 0
|
||||
|
||||
def generate_world_book_snapshot(self) -> Dict:
|
||||
"""生成 World Book 快照(供 PM 使用)"""
|
||||
snapshot = self.wb.export_snapshot()
|
||||
|
||||
# 添加策略师视角的元数据
|
||||
snapshot["strategist_view"] = {
|
||||
"strong_narratives": [
|
||||
n.topic for n in self.wb.get_top_narratives(3)
|
||||
],
|
||||
"macro_recommendation": self._get_macro_recommendation(),
|
||||
"risk_level": self._assess_risk_level()
|
||||
}
|
||||
|
||||
return snapshot
|
||||
|
||||
def _get_macro_recommendation(self) -> str:
|
||||
"""生成宏观建议"""
|
||||
cycle = self.wb.macro_cycle
|
||||
|
||||
if cycle.status == "upward" and cycle.liquidity == "loose":
|
||||
return "aggressive" # 激进配置
|
||||
elif cycle.status == "downward" or cycle.policy_wind == "regulation":
|
||||
return "defensive" # 防守配置
|
||||
else:
|
||||
return "balanced" # 平衡配置
|
||||
|
||||
def _assess_risk_level(self) -> str:
|
||||
"""评估市场风险等级"""
|
||||
# 基于叙事数量和宏观状态
|
||||
narrative_count = len(self.wb.active_narratives)
|
||||
cycle = self.wb.macro_cycle
|
||||
|
||||
if cycle.status == "downward" or narrative_count < 3:
|
||||
return "high"
|
||||
elif cycle.status == "upward" and narrative_count >= 5:
|
||||
return "low"
|
||||
else:
|
||||
return "medium"
|
||||
|
||||
|
||||
# ==================== 测试代码 ====================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 50)
|
||||
print("Skill C: Strategist 宏观策略师测试")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建 WorldBook 和 Strategist
|
||||
wb = WorldBook(data_dir="data")
|
||||
strategist = MacroStrategist(wb)
|
||||
|
||||
# 模拟 Analyst 输出
|
||||
mock_narrative_json = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"total_news": 3,
|
||||
"narratives": [
|
||||
{
|
||||
"topic": "低空经济",
|
||||
"news_count": 2,
|
||||
"avg_score": 82.0,
|
||||
"max_score": 90.0,
|
||||
"level_a_count": 0,
|
||||
"related_etfs": ["512980", "159969"],
|
||||
"lifecycle_stage": "fermentation"
|
||||
},
|
||||
{
|
||||
"topic": "AI算力",
|
||||
"news_count": 1,
|
||||
"avg_score": 75.0,
|
||||
"max_score": 75.0,
|
||||
"level_a_count": 0,
|
||||
"related_etfs": ["515980", "159813"],
|
||||
"lifecycle_stage": "fermentation"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 1. 处理叙事 JSON
|
||||
print("\n1. 处理叙事 JSON:")
|
||||
strategist.process_narrative_json(mock_narrative_json)
|
||||
|
||||
# 2. 显示当前 World Book 状态
|
||||
print("\n2. 当前 World Book 状态:")
|
||||
print(f" 活跃叙事数: {len(wb.active_narratives)}")
|
||||
print(f" 宏观周期: {wb.macro_cycle.to_dict()}")
|
||||
|
||||
# 3. 执行每日维护
|
||||
print("\n3. 执行每日维护:")
|
||||
maintenance_report = strategist.daily_maintenance()
|
||||
print(f" 维护报告: {json.dumps(maintenance_report, ensure_ascii=False, indent=2)}")
|
||||
|
||||
# 4. 检测宏观周期变化(模拟降准新闻)
|
||||
print("\n4. 检测宏观周期变化:")
|
||||
mock_news_with_policy = {
|
||||
"narratives": [
|
||||
{"topic": "央行降准", "level_a_count": 1, "avg_score": 95.0}
|
||||
]
|
||||
}
|
||||
|
||||
cycle_change = strategist.detect_macro_cycle_change(mock_news_with_policy)
|
||||
if cycle_change:
|
||||
print(f" 检测到周期变化: {cycle_change['reason']}")
|
||||
strategist.apply_macro_cycle_change(cycle_change)
|
||||
|
||||
# 5. 生成快照
|
||||
print("\n5. 生成 World Book 快照:")
|
||||
snapshot = strategist.generate_world_book_snapshot()
|
||||
print(f" 策略建议: {snapshot['strategist_view']['macro_recommendation']}")
|
||||
print(f" 风险等级: {snapshot['strategist_view']['risk_level']}")
|
||||
|
||||
# 6. 保存 World Book
|
||||
wb.save()
|
||||
|
||||
print("\n✅ Strategist 模块测试完成")
|
||||
199
test_vector_calculation.py
Normal file
199
test_vector_calculation.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""
|
||||
测试向量点积计算逻辑
|
||||
======================
|
||||
验证 Strategist 推导的宏观因子向量与 PM 的敏感度矩阵计算是否正确
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from core.world_book import WorldBook
|
||||
from skills.strategist import MacroStrategist
|
||||
from skills.pm import PortfolioManager
|
||||
|
||||
|
||||
def test_macro_vector_calculation():
|
||||
"""测试宏观因子向量计算"""
|
||||
print("=" * 60)
|
||||
print("测试 1: 宏观因子向量推导")
|
||||
print("=" * 60)
|
||||
|
||||
# 创建 WorldBook
|
||||
wb = WorldBook(data_dir="data/test")
|
||||
|
||||
# 创建 Strategist
|
||||
strategist = MacroStrategist(wb)
|
||||
|
||||
# 模拟新闻分析结果
|
||||
narrative_json = {
|
||||
"narratives": [
|
||||
{
|
||||
"topic": "央行宣布全面降息,支持数字经济发展",
|
||||
"level_a_count": 1,
|
||||
"level_b_count": 0,
|
||||
"avg_score": 85,
|
||||
"max_score": 90,
|
||||
"related_etfs": ["515230"],
|
||||
"lifecycle_stage": "fermentation"
|
||||
},
|
||||
{
|
||||
"topic": "地缘紧张局势升级,避险情绪升温",
|
||||
"level_a_count": 0,
|
||||
"level_b_count": 1,
|
||||
"avg_score": 70,
|
||||
"max_score": 75,
|
||||
"related_etfs": ["518880"],
|
||||
"lifecycle_stage": "incubation"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 推导宏观因子向量
|
||||
print("\n输入新闻:")
|
||||
for n in narrative_json['narratives']:
|
||||
print(f" - {n['topic']} (Level A: {n['level_a_count']}, Level B: {n['level_b_count']})")
|
||||
|
||||
print("\n推导宏观因子向量:")
|
||||
factor_vector = strategist.infer_macro_factor_vector(narrative_json)
|
||||
|
||||
print(f"\n结果: {factor_vector}")
|
||||
|
||||
return wb, factor_vector
|
||||
|
||||
|
||||
def test_sensitivity_calculation(wb: WorldBook, factor_vector: dict):
|
||||
"""测试敏感度矩阵计算"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 2: 向量点积计算")
|
||||
print("=" * 60)
|
||||
|
||||
# 创建 PM
|
||||
pm = PortfolioManager(wb, total_capital=1000000)
|
||||
|
||||
# 测试案例 1: 软件ETF
|
||||
print("\n案例 1: 软件与数字经济 (tech_software)")
|
||||
print("-" * 60)
|
||||
macro_score_software = pm.calculate_macro_sensitivity_score("tech_software")
|
||||
print(f"最终得分: {macro_score_software}")
|
||||
|
||||
# 测试案例 2: 银行ETF
|
||||
print("\n案例 2: 银行/高股息 (finance_bank)")
|
||||
print("-" * 60)
|
||||
macro_score_bank = pm.calculate_macro_sensitivity_score("finance_bank")
|
||||
print(f"最终得分: {macro_score_bank}")
|
||||
|
||||
# 测试案例 3: 黄金ETF
|
||||
print("\n案例 3: 资源与黄金 (resources_gold)")
|
||||
print("-" * 60)
|
||||
macro_score_gold = pm.calculate_macro_sensitivity_score("resources_gold")
|
||||
print(f"最终得分: {macro_score_gold}")
|
||||
|
||||
# 对比分析
|
||||
print("\n" + "=" * 60)
|
||||
print("对比分析")
|
||||
print("=" * 60)
|
||||
print(f"软件ETF得分: {macro_score_software}")
|
||||
print(f"银行ETF得分: {macro_score_bank}")
|
||||
print(f"黄金ETF得分: {macro_score_gold}")
|
||||
|
||||
print("\n结论:")
|
||||
if macro_score_software > macro_score_bank:
|
||||
print("✓ 软件ETF > 银行ETF (符合预期: 降息利好成长股,利空银行息差)")
|
||||
|
||||
if macro_score_gold > 0:
|
||||
print("✓ 黄金ETF得分为正 (符合预期: 地缘紧张利好黄金避险)")
|
||||
|
||||
|
||||
def test_trust_index_calculation(wb: WorldBook):
|
||||
"""测试完整的 Trust Index 计算"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 3: Trust Index 计算")
|
||||
print("=" * 60)
|
||||
|
||||
pm = PortfolioManager(wb, total_capital=1000000)
|
||||
|
||||
# 模拟叙事评分和资金流评分
|
||||
test_cases = [
|
||||
{
|
||||
"etf_code": "515230",
|
||||
"asset_id": "tech_software",
|
||||
"name": "软件ETF",
|
||||
"narrative_score": 85,
|
||||
"flow_score": 70
|
||||
},
|
||||
{
|
||||
"etf_code": "512800",
|
||||
"asset_id": "finance_bank",
|
||||
"name": "银行ETF",
|
||||
"narrative_score": 50,
|
||||
"flow_score": 60
|
||||
},
|
||||
{
|
||||
"etf_code": "518880",
|
||||
"asset_id": "resources_gold",
|
||||
"name": "黄金ETF",
|
||||
"narrative_score": 70,
|
||||
"flow_score": 55
|
||||
}
|
||||
]
|
||||
|
||||
print("\n计算各标的 Trust Index:")
|
||||
results = []
|
||||
|
||||
for case in test_cases:
|
||||
print(f"\n{case['name']} ({case['etf_code']}):")
|
||||
print(f" 叙事评分: {case['narrative_score']}")
|
||||
print(f" 资金流评分: {case['flow_score']}")
|
||||
|
||||
trust_result = pm.calculate_trust_index(
|
||||
case['etf_code'],
|
||||
case['asset_id'],
|
||||
case['narrative_score'],
|
||||
case['flow_score']
|
||||
)
|
||||
|
||||
results.append({**case, **trust_result})
|
||||
|
||||
print(f" → Trust Index: {trust_result['trust_index']} ({trust_result['verdict']})")
|
||||
|
||||
# 排序
|
||||
results.sort(key=lambda x: x['trust_index'], reverse=True)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("最终排名")
|
||||
print("=" * 60)
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f"{i}. {result['name']} - Trust Index: {result['trust_index']} "
|
||||
f"(Macro: {result['macro_score']:.2f}, Narrative: {result['narrative_score']}, "
|
||||
f"Flow: {result['flow_score']}) → {result['verdict']}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("\n" + "🧪 " * 30)
|
||||
print("MND-IA 向量点积计算测试")
|
||||
print("🧪 " * 30)
|
||||
|
||||
try:
|
||||
# 测试 1: 宏观因子向量推导
|
||||
wb, factor_vector = test_macro_vector_calculation()
|
||||
|
||||
# 测试 2: 敏感度矩阵点积计算
|
||||
test_sensitivity_calculation(wb, factor_vector)
|
||||
|
||||
# 测试 3: 完整 Trust Index 计算
|
||||
test_trust_index_calculation(wb)
|
||||
|
||||
print("\n" + "✅ " * 30)
|
||||
print("所有测试完成!")
|
||||
print("✅ " * 30)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user