#!/usr/bin/env python3 """注入持仓价格数据 + 多周期技术面 + 综合分类 + 大盘指数到 LLM 上下文。 每个tick自动执行,输出直接注入prompt上下文。""" import json import sys import os PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json" WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json" MTF_CACHE_PATH = "/home/hmo/web-dashboard/data/multi_tf_cache.json" def calc_ma(prices, window): if len(prices) < window: return None return round(sum(prices[-window:]) / window, 2) def fmt_holding(h): name = h.get("name", "?") code = h.get("code", "?") p = h.get("price", 0) chg = h.get("change_pct") chg_str = f"{chg:+.2f}%" if chg is not None else "N/A" pos = h.get("position_pct", 0) cost = h.get("cost") if cost and cost > 0: pnl = (p - cost) / cost * 100 pnl_str = f"{pnl:+.1f}%" else: pnl_str = "N/A" analysis = h.get("analysis", {}) or {} sl = analysis.get("stop_loss", "N/A") tp = analysis.get("take_profit", "N/A") return f" {name:<8}({code:<6}) 现价{p:<8} {chg_str:<8} 仓位{pos:<5.1f}% 盈亏{pnl_str:<8} 止损{sl} 止盈{tp}" def fmt_mtf(code, mtf_cache): stock = mtf_cache.get(code, {}) daily = stock.get("daily", []) if len(daily) < 5: return "" closes = [d["close"] for d in daily] ma5 = calc_ma(closes, 5) ma10 = calc_ma(closes, 10) if len(closes) >= 10 else None ma20 = calc_ma(closes, 20) if len(closes) >= 20 else None ma60 = calc_ma(closes, 60) if len(closes) >= 60 else None current = closes[-1] up_count = sum(1 for i in range(1, len(closes[-20:])) if closes[-20:][i] > closes[-20:][i-1]) up_ratio = up_count / min(len(closes), 20) if len(closes) >= 2 else 0.5 trend = "横盘" if up_ratio > 0.6 and ma20 and current > ma20: trend = "上升" elif up_ratio < 0.4 and ma20 and current < ma20: trend = "下降" ma_trend = "" if ma5 and ma10 and ma20: if ma5 > ma10 > ma20: ma_trend = "多头" elif ma5 < ma10 < ma20: ma_trend = "空头" above_ma20 = "↑" if ma20 and current > ma20 else ("↓" if ma20 and current < ma20 else "—") above_ma60 = "↑" if ma60 and current > ma60 else ("↓" if ma60 and current < ma60 else "—") parts = [f"趋势:{trend}"] if ma_trend: parts.append(ma_trend) if ma5: parts.append(f"MA5={ma5}") if ma20: parts.append(f"MA20={ma20}{above_ma20}") if ma60: parts.append(f"MA60={ma60}{above_ma60}") recent_high = max(d["high"] for d in daily[-20:]) if len(daily) >= 20 else max(d["high"] for d in daily) recent_low = min(d["low"] for d in daily[-20:]) if len(daily) >= 20 else min(d["low"] for d in daily) parts.append(f"近20日:{recent_low}~{recent_high}") return " | ".join(parts) def fmt_index_line(code, name, mtf_cache): entry = mtf_cache.get(code) if not entry or not isinstance(entry, dict): return "" daily = entry.get("daily", []) if len(daily) < 5: return "" closes = [d["close"] for d in daily] current = closes[-1] ma20 = calc_ma(closes, 20) or 0 ma60 = calc_ma(closes, 60) or 0 ma20_sig = "↑" if current > ma20 else "↓" ma60_sig = "↑" if current > ma60 else "↓" up_count = sum(1 for i in range(1, min(21, len(closes))) if closes[-i] > closes[-i-1]) trend = "上升" if up_count > 12 else ("下降" if up_count < 8 else "横盘") return f"{name}{current:.0f} {trend} MA20{ma20_sig} MA60{ma60_sig}" def classify_from_cache(code, name, mtf_cache): stock = mtf_cache.get(code, {}) daily = stock.get("daily", []) fund = stock.get("fundamentals", {}) if not daily or len(daily) < 5: return "" closes = [d["close"] for d in daily] current = closes[-1] ma20 = calc_ma(closes, 20) or 0 ma60 = calc_ma(closes, 60) or 0 recent_high = max(d["high"] for d in daily[-20:]) recent_low = min(d["low"] for d in daily[-20:]) volatility = ((recent_high - recent_low) / recent_low * 100) if recent_low > 0 else 0 pe = fund.get("pe") or 0 eps = fund.get("eps") or 0 mcap = fund.get("mcap_total") or 0 is_high_vol = volatility > 30 is_high_pe = pe > 100 or pe < 0 is_value = 0 < pe < 20 and eps > 0.5 category = "中短线" time_horizon = "2周~3月" suggestion = "中等仓位" if is_high_vol and is_high_pe: category = "短炒" time_horizon = "数日~2周" suggestion = "小仓快进快出" elif current < ma20 and current < ma60: category = "弱势" time_horizon = "观望" suggestion = "减仓或观望" elif is_value and current > ma20: category = "中长线" time_horizon = "数月~1年" suggestion = "正常配置" elif mcap > 1000 and eps > 0: category = "中长线" time_horizon = "数月~1年" suggestion = "正常配置" elif is_high_vol: category = "中短线" time_horizon = "2~6周" suggestion = "中等仓位" ma20_sig = "↑" if current > ma20 else "↓" ma60_sig = "↑" if current > ma60 else "↓" parts = [f"分类:{category}", f"周期:{time_horizon}", f"MA20{ma20_sig} MA60{ma60_sig}"] if pe and pe > 0: parts.append(f"PE={pe:.0f}") if eps and eps > 0: parts.append(f"EPS={eps:.2f}") parts.append(f"建议:{suggestion}") return " | ".join(parts) def main(): try: pf = json.load(open(PORTFOLIO_PATH)) except Exception as e: print(f"[ERROR] {e}") sys.exit(0) mtf_cache = load_mtf_cache() holdings = pf.get("holdings", []) updated_at = pf.get("updated_at", "unknown") # ===== 大盘指数趋势 ===== index_map = { "__index__sh000001": "上证", "__index__sz399001": "深证", "__index__sz399006": "创业板", "__index__sh000688": "科创50", "__index__hkHSI": "恒指", "__index__hkHSTECH": "恒科", } parts = [] for ic, nm in index_map.items(): line = fmt_index_line(ic, nm, mtf_cache) if line: parts.append(line) if parts: print("[大盘] " + " | ".join(parts)) print() # ===== 市场总览 ===== print(f"[持仓] 更新 {updated_at}") print(f"总资产:{pf.get('total_assets',0):.0f} | 市值:{pf.get('stock_value',0):.0f} | 现金:{pf.get('cash',0):.0f} | 仓位:{pf.get('position_pct',0):.1f}% | 日盈亏:{pf.get('day_pnl',0):+.0f}") print() a_h = [h for h in holdings if h.get("code","").startswith(("6","0","3")) and len(h.get("code",""))==6] hk_h = [h for h in holdings if h not in a_h] a_h.sort(key=lambda h: h.get("position_pct",0), reverse=True) hk_h.sort(key=lambda h: h.get("position_pct",0), reverse=True) print("=== A股持仓 ===") for h in a_h: print(fmt_holding(h)) print() print("=== 港股持仓 ===") for h in hk_h: print(fmt_holding(h)) # ===== 多周期技术面 ===== print() print("=== 多周期技术面 ===") for h in a_h + hk_h: line = fmt_mtf(h["code"], mtf_cache) if line: print(f" {h['name']:<8}({h['code']:<6}) {line}") # ===== 综合分类 ===== print() print("=== 综合分类 ===") for h in a_h + hk_h: line = classify_from_cache(h["code"], h["name"], mtf_cache) if line: print(f" {h['name']:<8}({h['code']:<6}) {line}") # ===== 自选股买入区监控 ===== try: wl = json.load(open(WATCHLIST_PATH)) wl_stocks = wl.get("stocks", []) # 过滤掉已经在持仓里的 held_codes = {h["code"] for h in holdings} watch_only = [s for s in wl_stocks if s.get("code") not in held_codes] # 只列出有买入区的 with_zone = [s for s in watch_only if s.get("analysis", {}).get("entry_low")] if with_zone: print() print("=== 自选买入区 ===") for s in with_zone: print(fmt_watchlist_item(s)) except Exception: pass print() print("--- end data inject ---") def fmt_watchlist_item(s): """格式化自选股条目""" name = s.get("name", "?") code = s.get("code", "?") price = s.get("price", 0) analysis = s.get("analysis", {}) or {} el = analysis.get("entry_low") eh = analysis.get("entry_high") sl = analysis.get("stop_loss") tp = analysis.get("take_profit") action = analysis.get("action", "")[:60] in_zone = "" if el and eh and price: if el <= price <= eh: in_zone = "✅在买入区" elif price < el: off = (el - price) / el * 100 in_zone = f"⬇低于买入区{off:.0f}%" else: off = (price - eh) / eh * 100 in_zone = f"⬆高于买入区+{off:.0f}%" return f" {name:<8}({code:<6}) ¥{price:<8} | {in_zone} | 买{el}~{eh} | 损{sl} 盈{tp} | {action}" def load_mtf_cache(): try: return json.load(open(MTF_CACHE_PATH)) except Exception: return {} if __name__ == "__main__": main()