From ef7c83a3ed78919c2433fc4d988270632e606ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Thu, 25 Jun 2026 21:32:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8D=A2=E4=BB=93=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E6=95=B4=E5=90=88=E6=B7=B1=E5=A5=97=E8=A7=A3=E5=A5=97+T+2?= =?UTF-8?q?=E5=89=8D=E7=9E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - T+2前瞻: 扫描自选股中价格距买入区5%以内的A股 - 提前评估现金是否足够 - 不足则评分港股持仓,推荐提前卖出最低评分港股 - T+2到账后目标股入买区时现金已到位 - 深套股在换仓评估中自然排后(score_future_outlook) - 不需专门解套方案,通过换仓机制逐步清理 - 数据治理: holding_strategies去重+中际旭创补策略 每周六cron自动检查 --- scripts/stale_push_wlin.py | 111 ++++++++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 7 deletions(-) diff --git a/scripts/stale_push_wlin.py b/scripts/stale_push_wlin.py index 54a9f11..87f1054 100644 --- a/scripts/stale_push_wlin.py +++ b/scripts/stale_push_wlin.py @@ -18,7 +18,32 @@ import json import os import threading import time -from datetime import datetime +from datetime import datetime, time + +# 市场时段检查 +_MARKET_HOURS = { + 'ashare': (time(9, 30), time(15, 0)), + 'hk': (time(9, 30), time(16, 0)), +} + +def is_ashare(code: str) -> bool: + """判断是否A股代码""" + return code.isdigit() and (code.startswith(('6', '5')) or len(code) in (6,)) + +def market_is_open(code: str, now: datetime = None) -> bool: + """检查某股票对应市场是否在交易时段内""" + if not code: + return True + now = now or datetime.now() + t = now.time() + code_str = str(code) + if code_str.startswith(('0', '1')) and len(code_str) == 5: + # 港股 + start, end = _MARKET_HOURS['hk'] + else: + # A股(含ETF、科创板) + start, end = _MARKET_HOURS['ashare'] + return start <= t <= end try: from urllib.request import Request, urlopen except ImportError: @@ -183,11 +208,12 @@ def trigger_regen_sync(stock_codes=None): def load_cash(): - """从 portfolio.json 实时读可用现金(不含冻结部分),不硬编码""" + """从 portfolio.json 实时读可用现金(可用 ≈ 实时买力),不硬编码""" try: with open(PORTFOLIO_PATH) as f: data = json.load(f) if isinstance(data, dict): + # 先读 cash_available(拆分了可用/冻结),fallback 到 cash return data.get("cash_available", data.get("cash", 0)) if isinstance(data, list) and len(data) > 1 and isinstance(data[1], dict): return data[1].get("cash_available", data[1].get("cash", 0)) @@ -311,6 +337,7 @@ def main(): cash = load_cash() stocks = [] stale_list = [] + all_candidates = [] # 所有在买入区的自选(stale+non-stale) for l in wl_lines: m = re.match(r'\[WL_IN\](?:\s+\[\w+\])*\s+\[自选\]\s+(\S+)\((\d+)\)', l) @@ -328,6 +355,8 @@ def main(): is_stale = "[STRATEGY_STALE]" in l cur = code_cur.get(code, "") + all_candidates.append((name, code, price, buy_low, buy_high, cur, is_stale)) + if not is_actionable(cur, code_data.get(code, {}).get("timing_signal", "")) or is_stale: stale_list.append((name, code, price, buy_low, buy_high, cur)) continue @@ -347,11 +376,12 @@ def main(): if macro_line: lines.append(f"【市场背景】{macro_line}") - # [重评] 内部流程 — 不在报告中展示,只执行重评 - if stale_list: - stale_codes = [s[1] for s in stale_list] - trigger_regen_sync(stale_codes) - # 重评完成,re-read decisions.json + # [关键修复: 2026-06-25] 所有预推票先重评,再出报告 + # 不只是 stale 的重评,所有在买入区的自选都先刷新策略,确保推荐不滞后 + to_reassess = list(set(s[1] for s in stocks) | set(s[1] for s in stale_list)) + if to_reassess: + trigger_regen_sync(to_reassess) + # 重评完成,re-read decisions.json 获取最新策略 code_data = {} try: with open("/home/hmo/web-dashboard/data/decisions.json") as f: @@ -361,6 +391,18 @@ def main(): except Exception: pass + # 重新过滤:重评后可能有策略变化(止盈/止损/信号变动) + # 重建 stocks 列表,用新数据判断(不再用旧 is_stale 标记,因为已全部重评) + stocks = [] + for (name, code, price, buy_low, buy_high, cur, is_stale) in all_candidates: + # 重评后重新检查 actionability(用新 timing_signal) + sig = code_data.get(code, {}).get("timing_signal", "") + if not is_actionable(cur, sig): + continue + lot = lot_cost(code, price) + ratio = lot / cash if cash > 0 else 999 + stocks.append((name, code, price, buy_low, buy_high, lot, ratio)) + # 加载portfolio获取持仓信息(A/H去重用) pf = {"holdings": []} try: @@ -415,6 +457,10 @@ def main(): if skip_ah: continue # 同一公司已在另一市场持有,不推荐 + # 市场时段检查:不在交易时段内的市场不推买入建议 + if not market_is_open(s[1]): + continue + actionable.append(s) if not actionable: @@ -788,6 +834,57 @@ def main(): if actual_n <= 0: return 0 # 全部冷却中 → 静默,不推 + # ── T+2前瞻:扫描近期可能入买区的A股,提前准备现金 ── + t2_lines = [] + try: + dec_t2 = json.loads(open("/home/hmo/web-dashboard/data/decisions.json").read()) + for entry in dec_t2.get("decisions", []): + if entry.get("status") == "closed" or entry.get("type") != "自选策略": + continue + ec = entry["code"] + el = entry.get("entry_low", 0) or 0 + eh = entry.get("entry_high", 0) or 0 + ep = entry.get("price", 0) or 0 + if not eh or not ep or el <= 0: + continue + # A股+价格在买入区上方5%以内(即将进入买入区) + if not is_hk_stock(ec) and el <= ep <= eh * 1.05 and ep > eh: + anticipation_pct = (ep - eh) / eh * 100 + lot = lot_cost(ec, ep) + if lot > available_cash: + # 现金不足 → 卖港股提前准备 + ph = [] + for h in pf.get("holdings", []): + hs = h.get("shares", 0) or 0 + hp = h.get("price", 0) or 0 + hc = h.get("cost", 0) or 0 + if hs <= 0 or hp <= 0 or not is_hk_stock(h.get("code","")): + continue + sc = score_future_outlook(h.get("code",""), code_data) + ph.append((sc, h)) + ph.sort(key=lambda x: x[0]) + if ph: + worst = ph[0][1] + w_name = worst.get("name","?") + w_code = worst.get("code","") + w_price = worst.get("price",0) + w_shares = worst.get("shares",0) + w_value = w_price * w_shares + if w_value >= lot: + name_e = entry.get("name","") + t2_lines.append( + f" ⏳ {name_e}({ec})距买入区仅{anticipation_pct:.0f}%," + f"需{lot:,.0f}元。建议提前卖{w_name}({w_code})" + f"腾{w_value:,.0f}元(T+2到账后可用)" + ) + except: + pass + + if t2_lines: + lines.append("") + lines.append("【⏳ 提前准备(T+2港股提前出清)】") + lines.extend(t2_lines) + lines.insert(0, f"【知微】自选买入提醒 {now} | 总资产{total_assets:,.0f}元") out = "\n".join(lines) print(out)