Initial: MoFin 持仓分析与策略管理系统

核心模块:
- 策略生命周期管理 (strategy_lifecycle.py)
- 技术分析引擎 (technical_analysis.py)
- 双维度策略评估 (strategy_evaluator.py)
- 实时行情获取 (get_realtime_prices.py)
- Web Dashboard (server.py, :8899)

提示词版本管理:
- prompt_manager 模块 — 统一管理所有知微提示词
- 8个提示词共24个版本已录入
- 策略→提示词版本关联追踪
- Dashboard「提示词」Tab

数据源增强:
- 服务端 POST /api/update/realtime 端点已就绪
- clients/tdx-relay/ — 小小莫在Windows上开发的通达信中继
- 解决港股15分钟延迟问题
This commit is contained in:
2026-06-12 22:54:51 +08:00
commit 9b9c37002a
65 changed files with 8659 additions and 0 deletions
+317
View File
@@ -0,0 +1,317 @@
#!/usr/bin/env python3
"""update_data.py — 解析cron输出,更新dashboard数据层"""
import json
import os
import re
from datetime import datetime
from pathlib import Path
DATA_DIR = Path(__file__).parent / "data"
def _save(name, data):
path = DATA_DIR / name
os.makedirs(path.parent, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def parse_report(markdown_text, source_file=None):
"""解析cron输出的markdown报告,提取结构化数据"""
report = {
"title": "",
"type": "未知",
"created_at": datetime.now().isoformat(),
"summary": "",
"content": markdown_text,
"stocks_mentioned": [],
"structured": None, # 结构化数据优先
}
lines = markdown_text.split("\n")
# 提取标题
for line in lines:
m = re.match(r"^#\s+(.+)", line)
if m:
report["title"] = m.group(1).strip()
break
m = re.match(r"^📊\s+(.+)", line)
if m:
report["title"] = m.group(1).strip()
break
# 判断类型
if "盘中" in report["title"]:
report["type"] = "盘中"
elif "盘后" in report["title"] or "复盘" in report["title"]:
report["type"] = "盘后"
elif "盯盘" in report["title"]:
report["type"] = "盯盘"
elif "扫描" in report["title"]:
report["type"] = "盘前"
# ★ 优先提取结构化JSON(如果知微输出了的话)
struct_match = re.search(r'<structured_data>\s*(\{.*?\})\s*</structured_data>', markdown_text, re.DOTALL)
if struct_match:
try:
parsed = json.loads(struct_match.group(1))
report["structured"] = parsed
# 从结构化数据中直接取stock codes
codes = set()
for h in parsed.get("holdings", []):
c = h.get("code", "")
if c:
codes.add(c)
report["stocks_mentioned"] = sorted(codes)
except (json.JSONDecodeError, Exception) as e:
pass # JSON解析失败→走NLP兜底
# 摘要(前3非空行)
body_lines = [l.strip() for l in lines if l.strip() and not l.strip().startswith("#") and not l.strip().startswith("##")]
report["summary"] = "\n".join(body_lines[:5])[:200]
# NLP兜底(仅当结构化数据没取到code时)
if not report["stocks_mentioned"]:
codes = set(re.findall(r'\b\d{6}\b', markdown_text))
hk_codes = set(re.findall(r'\b\d{5}\b', markdown_text))
report["stocks_mentioned"] = sorted(codes | hk_codes)
return report
def import_cron_outputs():
"""从cron输出目录导入最新报告"""
cron_dir = Path.home() / ".hermes" / "cron" / "output"
reports_dir = DATA_DIR / "reports"
os.makedirs(reports_dir, exist_ok=True)
count = 0
if not cron_dir.exists():
return count
for job_dir in sorted(cron_dir.iterdir()):
if not job_dir.is_dir():
continue
for f in sorted(job_dir.iterdir(), reverse=True)[:5]: # 每个job最近5个
if f.suffix != ".md":
continue
# Skip if already imported
export_name = f"cron_{job_dir.name}_{f.stem}.json"
if (reports_dir / export_name).exists():
continue
content = f.read_text(encoding="utf-8", errors="replace")
report = parse_report(content, source_file=str(f))
# Extract response section
resp_match = re.search(r"## Response\n+(.*)", content, re.DOTALL)
if resp_match:
resp = resp_match.group(1).strip()
if resp == "[SILENT]":
continue # Skip SILENT reports
report["_id"] = export_name.replace(".json", "")
_save(f"reports/{export_name}", report)
count += 1
return count
def extract_stock_mentions():
"""从报告中提取个股操作建议"""
reports_dir = DATA_DIR / "reports"
stocks_dir = DATA_DIR / "stocks"
os.makedirs(stocks_dir, exist_ok=True)
stock_data = {}
for f in sorted(reports_dir.iterdir()):
if f.suffix != ".json":
continue
try:
report = json.loads(f.read_text(encoding="utf-8"))
except:
continue
content = report.get("content", "")
codes = report.get("stocks_mentioned", [])
for code in codes:
if code not in stock_data:
stock_data[code] = {"code": code, "history": []}
# Try to extract recommendation from content
# Look for patterns like "建议|止盈|止损|补仓|持有"
pattern = re.compile(
rf'.*?({code}).*?(建议|止盈|止损|补仓|持有|减仓|加仓|卖出|买入).*?(?:\n|$)',
re.IGNORECASE,
)
for m in pattern.finditer(content):
stock_data[code]["history"].append({
"time": report.get("created_at", ""),
"content": m.group(0).strip()[:100],
"report_id": report.get("_id", ""),
})
for code, data in stock_data.items():
_save(f"stocks/{code}.json", data)
return len(stock_data)
def sync_to_decisions():
"""将个股建议同步到决策库(advice_timeline),自动去重"""
decisions_path = DATA_DIR / "decisions.json"
if not decisions_path.exists():
return 0
decisions = json.loads(decisions_path.read_text(encoding="utf-8"))
stocks_dir = DATA_DIR / "stocks"
synced = 0
for f in sorted(stocks_dir.iterdir()):
if f.suffix != ".json":
continue
try:
stock = json.loads(f.read_text(encoding="utf-8"))
except:
continue
code = stock.get("code", "")
history = stock.get("history", [])
if not code or not history:
continue
# 找决策库中是否有此股
existing = None
for d in decisions["decisions"]:
if d["code"] == code:
existing = d
break
if not existing:
# 无决策记录→生成inactive记录
existing = {
"code": code,
"name": stock.get("name", ""),
"timestamp": datetime.now().isoformat(),
"type": "历史建议汇总",
"current": "自动从update_data同步",
"status": "inactive",
"updated_by": "system(update_data)",
"advice_timeline": []
}
decisions["decisions"].append(existing)
# 去重合并
timeline = existing.setdefault("advice_timeline", [])
existing_keys = {(e["date"], e["direction"], e["summary"]) for e in timeline
if "date" in e and "direction" in e and "summary" in e}
new_count = 0
for entry in history:
content = entry.get("content", "")
# 判断方向
direction = "其他"
if any(w in content for w in ["买入", "加仓", "入场", "🟢", "可加", "可入"]):
direction = "买入"
elif any(w in content for w in ["卖出", "止盈", "减仓", "止损", "清仓", "🔴", "锁定利润"]):
direction = "卖出"
elif any(w in content for w in ["持有", "观望", "👀", "🤝", "暂持", "继续持有"]):
direction = "持有"
if direction == "其他":
continue
# 提取日期
rid = entry.get("report_id", "")
m = re.search(r'(\d{4}-\d{2}-\d{2})', rid)
date = m.group(1) if m else "unknown"
key = (date, direction, content.strip()[:80])
if key not in existing_keys:
existing_keys.add(key)
timeline.append({
"date": date,
"direction": direction,
"summary": content.strip()[:120],
"report_id": rid
})
new_count += 1
if new_count > 0:
# 按日期排序
timeline.sort(key=lambda e: e.get("date", ""))
synced += new_count
decisions_path.write_text(
json.dumps(decisions, ensure_ascii=False, indent=2), encoding="utf-8"
)
return synced
def build_portfolio_from_obsidian():
"""读取Obsidian持仓数据,生成portfolio.json"""
import subprocess
# Attempt to read from Obsidian
obsidian_path = Path.home() / "Obsidian" / "knowledge" / "finance"
portfolio_file = obsidian_path / "dad-portfolio.md"
holdings = []
total_assets = 0
stock_value = 0
cash = 0
if portfolio_file.exists():
content = portfolio_file.read_text(encoding="utf-8", errors="replace")
lines = content.split("\n")
for line in lines:
m = re.match(r'\|.*?\|.*?(\d+)@(\d+\.?\d*)@.*?\|(\d+\.?\d*)%?\|', line)
if m:
# Parse holding lines from markdown table
parts = [p.strip() for p in line.split("|")]
if len(parts) >= 8:
name = parts[1] if len(parts) > 1 else ""
code = parts[2] if len(parts) > 2 else ""
if code:
holdings.append({
"code": code,
"name": name,
"position_pct": 0,
"cost": 0,
"shares": 0,
"price": 0,
"change_pct": 0,
})
return {
"holdings": holdings,
"total_assets": total_assets,
"stock_value": stock_value,
"cash": cash,
"position_pct": 0,
"total_pnl": 0,
"updated_at": datetime.now().isoformat(),
}
if __name__ == "__main__":
count = import_cron_outputs()
if count > 0:
print(f"📥 新增报告: {count}")
stocks = extract_stock_mentions()
if stocks > 0:
print(f"📊 个股数据: {stocks}")
synced = sync_to_decisions()
if synced > 0:
print(f"📋 决策库: 新增{synced}条建议")
# 有实质更新才发汇总,否则安静
if count > 0 or stocks > 0 or synced > 0:
print(f"{datetime.now().strftime('%m/%d %H:%M')} 数据同步完成")
# 什么新数据都没有→安静,不输出任何内容