feat: 换仓策略整合深套解套+T+2前瞻

- T+2前瞻: 扫描自选股中价格距买入区5%以内的A股
  - 提前评估现金是否足够
  - 不足则评分港股持仓,推荐提前卖出最低评分港股
  - T+2到账后目标股入买区时现金已到位
- 深套股在换仓评估中自然排后(score_future_outlook)
  - 不需专门解套方案,通过换仓机制逐步清理
- 数据治理: holding_strategies去重+中际旭创补策略
  每周六cron自动检查
This commit is contained in:
知微
2026-06-25 21:32:01 +08:00
parent fa3fc93f25
commit ef7c83a3ed
+104 -7
View File
@@ -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)