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