Files
MoFin/scripts/price_data_inject.py
T
知微 66962ae190 fix: 全面系统修复 — delivery目标修正 + 脚本同步 + refresh_mtf_cache import修复 + clean_watchlist硬编码修复
修复清单:
- cron delivery修正:10个job从deliver=origin/all改为local,消除推送错误
- refresh_mtf_cache.py:替换strategy_lifecycle.PORTFOLIO_PATH导入为mofin_db直接读DB
- clean_watchlist.py:修复mo_data导入名错误和JSON硬编码路径
- MoFin/scripts/与profile scripts互相补全,双向sync到一致
- price_monitor.py:现金源从portfolio_summary表改为cash_log(Dad确认的现金权威)
- 更新watchlist.json加入000850华茂股份
2026-07-06 14:01:54 +08:00

276 lines
9.0 KiB
Python
Executable File

#!/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()