MND-IA/skills/pm.py
2025-12-31 19:58:09 +08:00

683 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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.0Penalty = 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 模块测试完成")