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:
@@ -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()
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""monitor_300308.py — 紧盯中际旭创(300308)站稳1330条件
|
||||
|
||||
每2分钟检查一次,条件满足时发XMPP信号,然后停用自身。
|
||||
|
||||
站稳1330条件(三条件同时满足):
|
||||
1. 现价 >= 1332(1330留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()
|
||||
Executable
+98
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user