sync: 全部cron脚本同步+游离代码归位+关键依赖部署

- 11个不一致cron脚本同步(MoFin→profile)
- 4个游离代码收入MoFin(300308_monitor/monitor_300308/refresh_mtf_cache/strategy-staleness-check)
- 6个关键依赖部署到profile(mo_models/mo_config/mo_provider/hk_rate/data_governance/data_validate)
This commit is contained in:
知微
2026-07-01 00:02:56 +08:00
parent 00b0e643d1
commit e8653acd37
6 changed files with 683 additions and 0 deletions
Binary file not shown.
Binary file not shown.
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""300308 午后紧盯监控 (no_agent)
每2分钟运行一次。在条件满足时输出买入信号,否则静默。
支持策略动态调整——输出状态变化。
"""
import json, os, sys, urllib.request
from pathlib import Path
from datetime import datetime
STATE_PATH = "/home/hmo/.hermes/300308_monitor_state.json"
BUY_ZONE_LOW = 1307.22
BUY_ZONE_HIGH = 1358.94
STOP_LOSS = 1293.88
TAKE_PROFIT = 1456.53
# 三档触发条件(按优先级)
LEVEL_STRONG = {"label": "★ 强信号", "price_min": 1330, "price_breach": 1325, "desc": "前3根5分K站稳1330+量放"}
LEVEL_MID = {"label": "◎ 中信号", "price_min": 1315, "price_breach": 1307, "desc": "回踩1315企稳+买盘放量"}
LEVEL_WEAK = {"label": "○ 弱信号", "price_min": 1307, "price_breach": 1298, "desc": "在买入区下沿附近企稳"}
def get_price():
url = "https://qt.gtimg.cn/q=sz300308"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
resp = urllib.request.urlopen(req, timeout=10)
data = resp.read().decode("gbk")
parts = data.split("~")
price = float(parts[3])
high = float(parts[33])
low = float(parts[34])
change_pct = float(parts[32])
volume = int(parts[6])
buy_vol = int(parts[7]) if parts[7] else 0
sell_vol = int(parts[8]) if parts[8] else 0
return {
"price": price,
"high": high,
"low": low,
"change_pct": change_pct,
"volume": volume,
"buy_vol": buy_vol,
"sell_vol": sell_vol,
}
def load_state():
try:
with open(STATE_PATH) as f:
return json.load(f)
except:
return {"phase": "waiting", "triggered_levels": [], "last_report": None, "afternoon_low": 99999}
def save_state(s):
os.makedirs(os.path.dirname(STATE_PATH), exist_ok=True)
with open(STATE_PATH, "w") as f:
json.dump(s, f, ensure_ascii=False, indent=2)
def main():
now = datetime.now()
# 只在13:00-15:00运行
if now.hour < 13 or now.hour >= 15:
# After 15:00, send final summary if not already sent
state = load_state()
if state.get("phase") not in ("done", "final"):
# Market closed, output final status
state["phase"] = "final"
save_state(state)
print("【300308紧盯结束】15:00收盘。")
# Final check
price = get_price()
if price["price"] >= STOP_LOSS:
print(f"收盘价{price['price']:.2f},未触及止损{STOP_LOSS},策略有效。")
else:
print(f"⚠️ 收盘价{price['price']:.2f}已跌破止损{STOP_LOSS}")
return
state = load_state()
# If already in done phase (signal already sent), stay silent
if state.get("phase") == "done":
return
price_data = get_price()
p = price_data["price"]
vol = price_data["volume"]
buy_vol = price_data["buy_vol"]
sell_vol = price_data["sell_vol"]
# Track afternoon low
if p < state["afternoon_low"]:
state["afternoon_low"] = p
# Get volume since last check to calculate relative activity
state.setdefault("last_vol", 0)
vol_delta = vol - state["last_vol"]
state["last_vol"] = vol
vol_active = vol_delta > 0 # new trades happened
# Determine buy/sell pressure (active buying as proportion)
total_trade = buy_vol + sell_vol
buy_ratio = buy_vol / total_trade if total_trade > 0 else 0.5
# Minutes into afternoon session
afternoon_minutes = (now.hour - 13) * 60 + now.minute
candle_num = afternoon_minutes // 5 + 1 # Which 5-min candle we're in
# ----------------------------------------------------------------
# Level checking - try from strong to weak
# ----------------------------------------------------------------
triggered = state.get("triggered_levels", [])
# Check levels from weak to strong
for level_key, level in [("weak", LEVEL_WEAK), ("mid", LEVEL_MID), ("strong", LEVEL_STRONG)]:
if level_key in triggered:
continue # already triggered, skip
if p >= level["price_min"] and state["afternoon_low"] >= level["price_breach"]:
# Basic price conditions met — check volume
# Volume should show active buying (buy_vol > sell_vol * 0.8 = not too one-sided)
volume_ok = buy_ratio >= 0.45 # at least 45% buying
if volume_ok:
triggered.append(level_key)
state["triggered_levels"] = triggered
if level_key == "strong":
msg = (
f"【300308买入信号】★ 强信号触发\n"
f"现价{p:.2f},已在1330上方站稳,午后最低{state['afternoon_low']:.2f}未破1325\n"
f"主动买比{buy_ratio:.0%},买盘积极\n"
f"距止损{(p-STOP_LOSS)/STOP_LOSS*100:.1f}%,距止盈{(TAKE_PROFIT-p)/p*100:.1f}%\n"
f"操作:买入1手(100股),止损{STOP_LOSS},止盈{TAKE_PROFIT}"
)
state["phase"] = "done"
save_state(state)
print(msg)
return
if level_key == "mid":
msg = (
f"【300308买入信号】◎ 中信号触发\n"
f"现价{p:.2f},回踩1315企稳,午后最低{state['afternoon_low']:.2f}\n"
f"主动买比{buy_ratio:.0%},买盘回温\n"
f"距止损{(p-STOP_LOSS)/STOP_LOSS*100:.1f}%,距止盈{(TAKE_PROFIT-p)/p*100:.1f}%\n"
f"操作:可买入1手(100股),止损{STOP_LOSS}(-{(p-STOP_LOSS)/p*100:.1f}%),止盈{TAKE_PROFIT}"
)
state["phase"] = "done"
save_state(state)
print(msg)
return
# ----------------------------------------------------------------
# Check for failure conditions
# ----------------------------------------------------------------
if state["afternoon_low"] < 1300: # Breaking towards stop loss
if not state.get("warned_low"):
state["warned_low"] = True
save_state(state)
print(f"【300308风险警告】午后最低{state['afternoon_low']:.2f},逼近止损{STOP_LOSS}(-4.4%)。"
f"现价{p:.2f}。建议取消今日买入。")
return
if state["afternoon_low"] < STOP_LOSS:
state["phase"] = "done" # stop monitoring
save_state(state)
print(f"【300308止损触发】午后最低{state['afternoon_low']:.2f}<止损{STOP_LOSS}"
f"策略已失效,取消今日买入。等明日重新评估。")
return
# ----------------------------------------------------------------
# Periodically report status (every ~10 mins)
# ----------------------------------------------------------------
last_report = state.get("last_report_min", -999)
report_interval = 10 # minutes
this_minute = now.hour * 60 + now.minute
if this_minute - last_report >= report_interval:
state["last_report_min"] = this_minute
# Check what condition we're closest to
if p >= LEVEL_STRONG["price_min"]:
closest = "强信号(1330)"
elif p >= LEVEL_MID["price_min"]:
closest = f"中信号(1315),差{LEVEL_STRONG['price_min']-p:.1f}到强信号"
else:
closest = f"弱信号(1307),差{LEVEL_MID['price_min']-p:.1f}到中信号"
msg = (
f"【300308午后监控】第{candle_num}根5分K线\n"
f"现价{p:.2f} | 午后最低{state['afternoon_low']:.2f}\n"
f"买比{buy_ratio:.0%} | 距目标{closest}\n"
f"仍有效,继续监控。"
)
save_state(state)
print(msg)
return
save_state(state)
# Silent - nothing to report
if __name__ == "__main__":
main()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""monitor_300308.py — 紧盯中际旭创(300308)站稳1330条件
每2分钟检查一次,条件满足时发XMPP信号,然后停用自身。
站稳1330条件(三条件同时满足):
1. 现价 >= 13321330留2元余量)
2. 连续2次检查都 >= 1332(排除毛刺)
3. 13:00之后(下午开盘后)
发一次信号后就停,不再重复。
"""
import json, os, subprocess, sys, urllib.request
from datetime import datetime
CODE = "300308"
NAME = "中际旭创"
PRICE_THRESHOLD = 1332.0
BUY_QTY = 100 # 1手
STOP_LOSS = 1293.88
TAKE_PROFIT = 1456.53
STATE_FILE = os.path.expanduser("~/.hermes/monitor_300308_state.json")
HERMES_SEND = ["hermes", "send", "--to", "xmpp:hmo@yoin.fun"]
def load_state():
try:
with open(STATE_FILE) as f:
return json.load(f)
except:
return {"alerted": False, "consecutive_ok": 0, "last_price": 0}
def save_state(s):
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(s, f, indent=2)
def fetch_price():
"""从腾讯API获取实时价"""
url = f"http://qt.gtimg.cn/q=sz{CODE}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
try:
resp = urllib.request.urlopen(req, timeout=10)
text = resp.read().decode("gbk")
fields = text.split("~")
price = float(fields[3]) if fields[3] else 0
change_pct = fields[32] if len(fields) > 32 else "0"
return price, change_pct
except Exception as e:
return 0, "0"
def send_alert(price, change_pct):
msg = (
f"【信号】{NAME}({CODE}) 站稳1330,可买入!\n"
f"现价{price} ({change_pct}%)\n"
f"信号:下午三条件满足(现价>=1332+连续确认+午后时段)\n"
f"操作:买入1手(100股)约{price*100:.0f}\n"
f"止损{STOP_LOSS}({(price-STOP_LOSS)/price*100:.1f}%) 止盈{TAKE_PROFIT}({(TAKE_PROFIT-price)/price*100:.1f}%)\n"
f"RR={(TAKE_PROFIT-price)/(price-STOP_LOSS):.1f}\n"
f"现金150,625元,1手占{price*100/150625*100:.1f}%"
)
try:
subprocess.run(HERMES_SEND + [msg], capture_output=True, timeout=15)
return True
except:
return False
def main():
now = datetime.now()
state = load_state()
# 已发过信号 → 静默
if state.get("alerted"):
print("[SILENT] 已发过买入信号")
return
# 非交易时段 → 静默(但允许14:30前)
if now.weekday() >= 5 or now.hour < 13 or now.hour >= 14:
print(f"[SILENT] 非监控时段(13:00-14:00)")
return
# 取价
price, change_pct = fetch_price()
if price == 0:
print(f"[SILENT] 取价失败")
return
# 条件1:现价 >= 1332
if price >= PRICE_THRESHOLD:
state["consecutive_ok"] = state.get("consecutive_ok", 0) + 1
else:
state["consecutive_ok"] = 0
print(f"[SILENT] 现价{price}低于阈值{PRICE_THRESHOLD}")
save_state(state)
return
# 条件2:连续2次检查都满足(约4分钟确认)
if state["consecutive_ok"] < 2:
print(f"[SILENT] 连续确认中 {state['consecutive_ok']}/2")
save_state(state)
return
# 条件3:13:00之后(已隐含在上面的时段检查中)
# 全部满足 → 发信号
success = send_alert(price, change_pct)
if success:
state["alerted"] = True
state["alerted_at"] = now.isoformat()
state["alert_price"] = price
save_state(state)
print(f"✅ 信号已推送: {NAME} {price} 可买入")
else:
print(f"⚠️ 信号生成失败,下次重试")
if __name__ == "__main__":
main()
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""多周期缓存刷新脚本 — 在开盘前预填充K线数据
为所有持仓+自选股预先拉取日/周/月K线,写入 multi_tf_cache.json
这样收盘后全量重评(regenerate_all)运行时K线数据已有缓存,无需逐个拉取。
运行时间:每天9:00(开盘前),no_agent模式。
无输出 = 成功(避免每天收到无意义消息)。
"""
import sys
import os
import json
from datetime import datetime
# 确保能找到 web-dashboard 模块
sys.path.insert(0, "/home/hmo/web-dashboard")
# 控制台UTC日志
def log(msg):
ts = datetime.utcnow().strftime("%H:%M:%S")
print(f"[{ts}] {msg}", file=sys.stderr)
def main():
from strategy_lifecycle import safe_json_load, PORTFOLIO_PATH, WATCHLIST_PATH
# 收集所有股票代码
codes = []
for item in safe_json_load(PORTFOLIO_PATH, {}).get("holdings", []):
code = item.get("code", "")
if code:
codes.append(("portfolio", code))
seen = set(c[1] for c in codes)
for item in safe_json_load(WATCHLIST_PATH, {}).get("stocks", []):
code = item.get("code", "")
if code and code not in seen:
codes.append(("watchlist", code))
seen.add(code)
# 加入指数代码(用于多周期趋势研判)
INDEXES = {
"sh000001": "上证指数", "sz399001": "深证成指",
"sz399006": "创业板指", "sh000688": "科创50",
"hkHSI": "恒生指数", "hkHSTECH": "恒生科技",
}
for idx_code in INDEXES:
if idx_code not in seen:
codes.append(("index", idx_code))
seen.add(idx_code)
log(f"Pre-populating multi-timeframe cache for {len(codes)} stocks...")
# 检查当前缓存,只更新需要更新的
mtf_cache_path = "/home/hmo/web-dashboard/data/multi_tf_cache.json"
try:
with open(mtf_cache_path) as f:
existing = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
existing = {}
import time
from multi_timeframe import full_multi_tf_analysis
cached = 0
fetched = 0
errors = 0
for source, code in codes:
cached_entry = existing.get(code, {})
updated_at = cached_entry.get("updated_at", 0)
now = time.time()
# 检查缓存是否新鲜:日K 1小时内,周/月K 1天内
has_daily = bool(cached_entry.get("daily"))
has_weekly = bool(cached_entry.get("weekly"))
has_monthly = bool(cached_entry.get("monthly"))
cache_fresh = (updated_at > 0 and (now - updated_at) < 3600)
if has_daily and has_weekly and has_monthly and cache_fresh:
cached += 1
continue
try:
r = full_multi_tf_analysis(code)
if any(k in r for k in ["daily", "weekly", "monthly"]):
fetched += 1
log(f" OK {code} ({source})")
else:
errors += 1
log(f" EMPTY {code} ({source})")
except Exception as e:
errors += 1
log(f" FAIL {code} ({source}): {e}")
log(f"Done: {cached} cached, {fetched} fetched, {errors} errors")
if __name__ == "__main__":
main()
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""
策略时效性检查 - Strategy Staleness Checker v1
==============================================
扫描所有活跃策略,检查三个维度:
1. 时间老化:超过14/21天未更新预警
2. 价格偏离:当前价偏离买入区中心 >30% 预警
3. 买入区失效:买入区完全脱离当前价格区间
输出两份:JSON报告(给系统) + 人类可读摘要(stdout for cron)
"""
import json, sys, os, re, urllib.request
from datetime import datetime
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
OUTPUT_PATH = "/home/hmo/web-dashboard/data/strategy_staleness_report.json"
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
# Fallback: if new path not found, use old path
FALLBACK_PATH = "/home/hmo/data/decisions.json"
WARN_DAYS = 14 # 超过14天未更新→警告
CRITICAL_DAYS = 21 # 超过21天→严重警告
DIVERGENCE_WARN = 30 # 偏离买入区>30%→警告
DIVERGENCE_CRIT = 50 # 偏离>50%→严重
def get_price(code):
"""从腾讯API获取当前价"""
try:
market = "sh" if code.startswith("6") else "sz" if code.startswith("0") or code.startswith("3") else ""
if code.startswith(("00", "30")) or code.startswith("68"):
market = "sh" if code.startswith("6") else "sz"
elif code.startswith(("01", "02", "03")):
market = "sz"
url = f"http://qt.gtimg.cn/q={market}{code}"
req = urllib.request.Request(url, headers={"User-Agent": "curl/7.81"})
with urllib.request.urlopen(req, timeout=5) as resp:
raw = resp.read().decode("gbk")
parts = raw.split("~")
if len(parts) > 3:
price = float(parts[3]) if parts[3] else 0
chg = float(parts[32]) if parts[32] else 0
return price, chg if price > 0 else (None, None)
except: pass
return None, None
def parse_buy_zone(current):
"""从策略current字段提取买入区间最低和最高"""
if not current:
return None, None
m = re.search(r'买入.*?(\d+\.?\d*)\s*[~\-]\s*(\d+\.?\d*)', current)
if m:
return float(m.group(1)), float(m.group(2))
return None, None
def main():
# Try new path first, fall back to old format
path = DECISIONS_PATH if os.path.exists(DECISIONS_PATH) else FALLBACK_PATH
is_new_format = (path == DECISIONS_PATH)
with open(path) as f:
data = json.load(f)
# Filter: exclude closed strategies
all_entries = data.get("decisions", [])
active = [e for e in all_entries if e.get("status", "") != "closed"]
flagged = []
now = datetime.now()
for entry in active:
code = entry.get("code", "")
name = entry.get("name", "") or code
entry_type = entry.get("type", "")
is_watchlist = "自选" in entry_type
# --- Age tracking: prefer created_at, fallback to timestamp/updated_at ---
ts = entry.get("created_at") or entry.get("timestamp") or entry.get("updated_at") or ""
age = 0
if ts:
try:
if "T" in ts:
dt = datetime.fromisoformat(ts)
else:
dt = datetime.strptime(ts, "%Y-%m-%d %H:%M:%S")
age = (now - dt).days
except:
try:
# Try alternative format
dt = datetime.strptime(str(ts)[:19], "%Y-%m-%d %H:%M:%S")
age = (now - dt).days
except:
pass
# --- Get price: prefer local data, API fallback ---
price = entry.get("price", None)
if not price or price == 0:
price, _ = get_price(code)
else:
price = float(price) if price else None
# --- Get entry zone from structured fields ---
entry_low = entry.get("entry_low", None)
entry_high = entry.get("entry_high", None)
if (not entry_low) or (not entry_high):
# Fall back to parsing from action/current string
txt = entry.get("action", "") or entry.get("current", "")
entry_low, entry_high = parse_buy_zone(txt)
# --- Build display info ---
action_text = entry.get("action", "") or entry.get("current", "")
last_update = entry.get("updated_at", "") or entry.get("timestamp", "")
flags = []
# Time-based flagging (different thresholds for 持仓 vs 自选)
time_threshold = WARN_DAYS if not is_watchlist else WARN_DAYS * 2 # 自选给2倍时间
critical_threshold = CRITICAL_DAYS if not is_watchlist else CRITICAL_DAYS * 2
if age >= critical_threshold and not is_watchlist:
flags.append(f"严重过期: {age}天未更新")
elif age >= time_threshold and is_watchlist:
flags.append(f"策略已{age}天未更新(自选)")
elif age >= time_threshold:
flags.append(f"策略已{age}天未更新")
if price and entry_low and entry_high:
# -- STRATEGY_STALE check for watchlist stocks in buy zone --
if is_watchlist and entry_low <= price <= entry_high:
timing_signal = entry.get("timing_signal", "") or ""
rr_ratio = entry.get("rr_ratio", 0) or 0
sl = entry.get("stop_loss", 0) or 0
tp = entry.get("take_profit", 0) or 0
# 规则1: timing_signal 含"弱势持有"/"等企稳" → STRATEGY_STALE
if any(kw in timing_signal for kw in ["弱势持有", "等企稳"]):
flags.append("[STRATEGY_STALE] 信号不良(timing_signal含"+str([kw for kw in ["弱势持有", "等企稳"] if kw in timing_signal])+")")
# 规则2: RR<1.5 或无止盈 → STRATEGY_STALE
if tp == 0 or (rr_ratio > 0 and rr_ratio < 1.5):
flags.append("[STRATEGY_STALE] 盈亏比不足(RR="+str(rr_ratio)+")或无止盈")
if is_watchlist:
# 自选股:检查价格是否进入了买入区
if entry_low <= price <= entry_high:
flags.append(f"现价{price:.2f}在买入区{entry_low:.0f}~{entry_high:.0f}(是否可买需结合timing_signal判断)")
elif price > entry_high * 1.3:
flags.append(f"现价{price:.2f}远高于买入区{entry_low:.0f}~{entry_high:.0f},需重评")
elif price < entry_low * 0.8:
flags.append(f"现价{price:.2f}远低于买入区{entry_low:.0f}~{entry_high:.0f},买入区需下移")
else:
# 持仓股:检查价格偏离买入区中心
zone_center = (entry_low + entry_high) / 2
if zone_center > 0:
divergence = abs(price - zone_center) / zone_center * 100
if divergence >= DIVERGENCE_CRIT:
flags.append(f"价格距买入区中心{divergence:.0f}% 已严重偏离")
elif divergence >= DIVERGENCE_WARN:
flags.append(f"价格距买入区中心{divergence:.0f}% 需关注")
# Check if price entirely out of zone
if price > entry_high * 1.5:
flags.append(f"现价{price:.0f}远高于买入区{entry_low:.0f}~{entry_high:.0f}")
elif price < entry_low * 0.5:
flags.append(f"现价{price:.0f}远低于买入区{entry_low:.0f}~{entry_high:.0f}")
# Add type tag for clarity
type_tag = "自选" if is_watchlist else "持仓"
if flags:
flagged.append({
"code": code,
"name": name,
"price": round(price, 2) if price else None,
"flags": flags,
"age_days": age,
"last_update": last_update,
"entry_zone": f"{entry_low:.0f}~{entry_high:.0f}" if entry_low else "无买入区",
"current": action_text,
"updated_by": entry.get("source", "auto"),
"updated_reason": "自动生成",
"is_watchlist": is_watchlist,
})
# --- Portfolio-level analysis: count weak + deep_loss categories ---
holdings_count = len([e for e in active if "自选" not in e.get("type", "")])
weak_count = len([e for e in active if e.get("stock_category") in ("弱势", "深套") and "自选" not in e.get("type", "")])
all_weak_count = len([e for e in active if e.get("stock_category") in ("弱势", "深套")])
weak_pct = round(weak_count / holdings_count * 100, 1) if holdings_count > 0 else 0
all_weak_pct = round(all_weak_count / len(active) * 100, 1) if len(active) > 0 else 0
# Read portfolio for position%
position_pct = 0
cash = 0
try:
with open(PORTFOLIO_PATH) as pf:
pdata = json.load(pf)
position_pct = pdata.get("position_pct", 0)
cash = pdata.get("cash", 0)
except: pass
portfolio_flags = []
if weak_pct >= 40:
portfolio_flags.append(f"[PORTFOLIO_WEAK] 组合中弱势+深套分类持仓占比{weak_pct}%>40%,建议系统性减仓")
elif weak_pct >= 30:
portfolio_flags.append(f"[PORTFOLIO_WEAK_MILD] 组合弱势占比{weak_pct}%,需关注")
if position_pct > 80:
portfolio_flags.append(f"[PORTFOLIO_FULL] 总仓位{position_pct}%(现金{cash:.0f}元),买入建议受限")
# Write report
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
report = {
"checked_at": now.strftime("%Y-%m-%dT%H:%M:%S"),
"total_active": len(active),
"flagged_count": len(flagged),
"flagged": flagged,
"portfolio": {
"position_pct": position_pct,
"cash": round(cash, 2),
"weak_position_pct": weak_pct,
"all_weak_pct": all_weak_pct,
"signals": portfolio_flags
},
"summary": f"扫描{len(active)}个策略,{len(flagged)}个需关注"
}
with open(OUTPUT_PATH, 'w') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
# Human-readable output
now_str = now.strftime("%m/%d %H:%M")
print(f"【策略重估检查】{now_str}")
print(f"活跃策略{len(active)}个 | 需关注{len(flagged)}")
# Portfolio-level signals first (if any)
for pf in portfolio_flags:
print(pf)
if flagged:
print("")
for s in flagged:
price_str = f"{s['price']}" if s['price'] else "N/A"
ttag = "[自选]" if s.get('is_watchlist') else ""
print(f"{ttag}[{s['code']}] {s['name']} {price_str}")
print(f" 上次更新{s['age_days']}天前 | 区间{s['entry_zone']}")
for f in s['flags']:
print(f"{f}")
print(f" 策略: {s['current']}")
print("")
print(f"—END—{len(flagged)}个需关注 | 弱势持仓占比{weak_pct}% | 仓位{position_pct}%")
return len(flagged)
if __name__ == "__main__":
try:
stale = main()
sys.exit(0) # Always exit 0 — flagged items are in JSON report + stdout; exit code 1 creates misleading "Script Error" alerts
except Exception as e:
print(f"ERROR: {e}")
sys.exit(2)