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华茂股份
This commit is contained in:
Executable
+275
@@ -0,0 +1,275 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user