diff --git a/data/decisions.json b/data/decisions.json index 487d535..44b0eb4 100644 --- a/data/decisions.json +++ b/data/decisions.json @@ -9623,5 +9623,5 @@ } ], "total": 42, - "regenerated_at": "2026-06-24 11:47" + "regenerated_at": "2026-06-24 11:54" } \ No newline at end of file diff --git a/data/portfolio.json b/data/portfolio.json index dde1c2d..fbc12a7 100644 --- a/data/portfolio.json +++ b/data/portfolio.json @@ -54,8 +54,8 @@ "action_note": "深套持有", "timing_signal": "持有" }, - "price": 7.66, - "change_pct": -2.79 + "price": 7.71, + "change_pct": -2.16 }, { "code": "600739", @@ -167,7 +167,7 @@ "action_note": "深套持有", "timing_signal": "持有" }, - "price": 98.85, + "price": 98.95, "change_pct": -0.1 }, { @@ -252,8 +252,8 @@ "action_note": "短炒强趋势持", "timing_signal": "持有" }, - "price": 96.3, - "change_pct": 10.37 + "price": 95.1, + "change_pct": 9.11 }, { "code": "02202", @@ -281,8 +281,8 @@ "action_note": "深套持有", "timing_signal": "持有" }, - "price": 2.37, - "change_pct": -1.25 + "price": 2.36, + "change_pct": -1.67 }, { "code": "02388", @@ -310,7 +310,7 @@ "action_note": "", "timing_signal": "持有" }, - "price": 46.26, + "price": 46.22, "change_pct": -1.57 }, { @@ -367,7 +367,7 @@ "action_note": "深套持有", "timing_signal": "持有" }, - "price": 74.8, + "price": 74.85, "change_pct": -1.38 }, { @@ -396,8 +396,8 @@ "action_note": "⚠️盈亏比偏低(1:1.0),不建议加仓", "timing_signal": "持有" }, - "price": 418.8, - "change_pct": 0.82 + "price": 419.0, + "change_pct": 1.06 }, { "code": "00981", @@ -425,8 +425,8 @@ "action_note": "", "timing_signal": "持有" }, - "price": 85.05, - "change_pct": 9.31 + "price": 84.75, + "change_pct": 9.12 }, { "code": "09868", @@ -454,8 +454,8 @@ "action_note": "⚠️盈亏比偏低(1:1.0),不建议加仓", "timing_signal": "持有" }, - "price": 50.0, - "change_pct": 1.22 + "price": 49.94, + "change_pct": 1.05 }, { "code": "600036", @@ -539,8 +539,8 @@ "action_note": "⚠️盈亏比偏低(1:1.2),不建议加仓", "timing_signal": "持有" }, - "price": 52.75, - "change_pct": -1.59 + "price": 52.8, + "change_pct": -1.4 }, { "code": "300035", @@ -653,7 +653,7 @@ "timing_signal": "持有" }, "price": 41.84, - "change_pct": -0.52 + "change_pct": -0.33 }, { "code": "600563", diff --git a/scripts/stale_push_wlin.py b/scripts/stale_push_wlin.py index 9fada99..2f3d277 100644 --- a/scripts/stale_push_wlin.py +++ b/scripts/stale_push_wlin.py @@ -395,26 +395,101 @@ def main(): return theo_pct, pct_actual, details, lots, lot_cost_total # ── 换仓评估 ────────────────────────────────────────────────────── - def evaluate_swap(lot_cost_target, rr, sig, tp, sl, name, code, price_in, total_assets_in, cash_in, pf_in): + def score_future_outlook(code_in, h_data, cd_in): + """基于决策系统已有的分析数据,评估持仓的未来前景评分。 + + 评分越低(负数越大)= 前景越差,越值得考虑卖出。 + 评分越高(正数越大)= 前景越好,应该保留。 + + 考虑维度(按重要度排序): + 1. timing_signal — 决策系统输出的主信号 + 2. 技术形态 — bearish/bullish/neutral + 3. 量价关系 — 买卖盘主导 + 4. 行业背景 — 板块强弱 + 5. 盈亏比RR — 预期收益/风险 + 6. 股票类别 — 蓝筹/深套/题材 + """ + d = cd_in.get(code_in, {}) + if not d: + return -999 # 无数据=前景未知,保守视为差 + + score = 0 + reasons = [] + + # 1. timing_signal — 最直接的信号 + signal = (d.get('timing_signal') or '').strip() + if '买入' in signal or '加仓' in signal: + score += 3 + reasons.append('有买入信号') + elif '深套持有' in signal or '弱势持有' in signal: + score -= 2 + reasons.append('深套/弱势持有') + elif signal in ('持有', '') or not signal: + score -= 0.5 # 中性偏弱(没有积极信号就是消极信号) + reasons.append('无积极信号') + + # 2. 技术形态 + tech = (d.get('tech_snapshot') or '') or '' + if '/bearish' in tech: + score -= 1.5 + reasons.append('技术偏空') + elif '/bullish' in tech: + score += 1.5 + reasons.append('技术偏多') + + # 3. 量价关系 + if '主动卖盘占优' in tech: + score -= 1 + reasons.append('卖盘主导') + elif '主动买盘占优' in tech: + score += 1 + reasons.append('买盘主导') + + # 4. 行业背景(大跌/大涨) + sector = (d.get('sector_context') or '') or '' + if '大跌' in sector or '偏弱' in sector: + score -= 0.5 + if '大涨' in sector or '偏强' in sector: + score += 0.5 + + # 5. 盈亏比RR + rr = d.get('rr_ratio', 0) or 0 + if rr >= 2: + score += 1 + reasons.append(f'RR{rr:.1f}≥2') + elif rr < 1: + score -= 0.5 + reasons.append(f'RR{rr:.1f}<1') + + # 6. 股票类别 + cat = (d.get('stock_category') or '') or '' + if '蓝筹' in cat or '白马' in cat: + score += 0.5 + elif '深套' in cat or '弱势' in cat: + score -= 0.5 + + return round(score, 1) + + def evaluate_swap(lot_cost_target, rr, sig, tp, sl, name, code, price_in, + total_assets_in, cash_in, pf_in, cd_in): """现金不足时评估是否卖差票换推荐股。 - 核心逻辑:已发生的亏损是沉没成本,不参与决策。 - 只比较:持有 old 票的未来预期 vs 换到 new 票的未来预期。 - - 对深套票(<-15%)默认未来预期收益≈-5%~0%(死钱)。 - 对目标票(RR>=3+买入信号)默认未来预期收益≈(tp-price)/price×胜率。 - 如果目标票的未来收益预期显著更好,推荐切换。 + 核心逻辑: + - 已发生的亏损是沉没成本,不参与决策 + - 用"全面分析"法评估每个持仓的未来前景(基于决策系统既有数据) + - 优先卖前景最差的票,保留前景好的票(无论当前盈亏%) + - 对目标票(RR>=3+买入信号)才有换仓资格 返回(推荐文案str, 缺口float)或 (None, gap) """ gap = lot_cost_target - cash_in - # 目标票质量门槛:换仓需要比普通买入更高的置信度 + # 目标票质量门槛 if rr < 3.0 or gap <= 0 or gap > total_assets_in * 0.5: return None, gap if not any(kw in sig for kw in ["买入", "加仓", "建仓"]): return None, gap - # 收集持仓盈亏数据 + # 收集持仓数据 + 前景评分 ph = [] for h in pf_in.get("holdings", []): hs = h.get("shares", 0) or 0 @@ -425,49 +500,67 @@ def main(): hmv = hs * hp h_code = str(h.get("code", "")) if len(h_code) <= 5: - hmv *= 0.866 + hmv *= 0.866 # approximate HKD→CNY hpl_pct = (hp - hc) / hc * 100 if hc else 0 - ph.append({"code": h_code, "name": h.get("name", ""), - "shares": hs, "price": hp, "cost": hc, - "mv": round(hmv), "pl_pct": round(hpl_pct, 1)}) + + # 全面评分(越低越差,越建议卖) + fscore = score_future_outlook(h_code, h, cd_in) + + ph.append({ + "code": h_code, + "name": h.get("name", ""), + "shares": hs, + "price": hp, + "cost": hc, + "mv": round(hmv), + "pl_pct": round(hpl_pct, 1), + "score": fscore, + }) - # 只考虑深套票(<-15%)作为减仓候候选 — 死钱,不如换到有信号的位置 - ph.sort(key=lambda x: x["pl_pct"]) - candidates = [h for h in ph if h["pl_pct"] < -15] + # 按前景评分升序(最差的排最前面) + ph.sort(key=lambda x: x["score"]) + + # 打印调试信息:所有持仓的前景评分 + # print(f"[SWAP_DEBUG] 前景评分(越低越差):", file=sys.stderr) + # for x in ph[:10]: + # print(f" {x['name']}({x['code']}) 评分{x['score']} 亏{x['pl_pct']}% 市值{x['mv']:,}", file=sys.stderr) + + # 只考虑评分<=0(前景差或中性偏弱)的作为减仓候选 + candidates = [h for h in ph if h["score"] <= 0] if not candidates: return None, gap - # 优先选"锁亏效率高"的(锁定亏损占市值比例低的) - # 效率 = mv / abs(pl) ,越大说明亏损占比越小 - for h in candidates: - h["eff"] = round(h["mv"] / max(abs(h["pl_pct"]) / 100 * h["mv"], 1), 1) - candidates.sort(key=lambda x: -x["eff"]) # 效率高的优先 - + # 贪心选评分最差的,凑够现金缺口(最多2只) selected = [] cash_freed = 0 - total_mv_sold = 0 for h in candidates: if cash_freed >= gap: break cash_freed += h["mv"] - total_mv_sold += h["mv"] selected.append(h) if cash_freed < gap or len(selected) > 2: return None, gap - # 计算目标票的预期涨幅(到期权价值) + # 计算目标票的预期涨幅 if tp and tp > 0: - target_gain_pct = (tp - price_in) / price_in * 100 # e.g. 16.9% + target_gain_pct = (tp - price_in) / price_in * 100 else: - target_gain_pct = rr * 3 # RR*3%~粗略 + target_gain_pct = rr * 3 - # 沉没成本不参与决策 — 直接描述方案 - sell_desc = ";".join( - f"{h['name']}({h['code']}) {h['shares']}股 亏{h['pl_pct']}%" - for h in selected - ) - sell_note = "、".join(h['name'] for h in selected) + # 构建推荐文案 + sell_parts = [] + sell_names = [] + for h in selected: + # 每个被选股票配一句"为什么卖它" + reason = f"前景评分{h['score']}" + if h['pl_pct'] <= -30: + reason += "深套" + elif h['pl_pct'] <= -15: + reason += f"亏损{h['pl_pct']}%" + sell_parts.append(f"{h['name']}({h['code']}) {h['shares']}股 亏{h['pl_pct']}% ({reason})") + sell_names.append(h['name']) + sell_desc = ";".join(sell_parts) new_budget = cash_in + cash_freed new_lots = int(new_budget / lot_cost_target) if lot_cost_target > 0 else 0 @@ -482,17 +575,6 @@ def main(): new_cost = new_lots * lot_cost_target new_pct = round(new_cost / total_assets_in * 100) if total_assets_in > 0 else 0 - # 盈亏回本期望对比 - # 深套票继续持有预期:这些票已深套恢复遥遥无期,继续持有预期收益≈-5%~0% - # 新票预期:止损-3%~止盈+target_gain_pct,RR指引正向 - # 更实用的表述:卖掉的票回本需涨多少 vs 新票上涨潜力 - recovery_needed = [] - for h in selected: - if h["pl_pct"] < 0: - recover = round(-h["pl_pct"] / (100 + h["pl_pct"]) * 100, 1) if h["pl_pct"] > -100 else 999 - recovery_needed.append(f"{h['name']}需涨{recover}%回本") - recover_str = ",".join(recovery_needed) - text = ( f"换仓建议:卖{sell_desc}" f"→腾{round(cash_freed):,}元" @@ -501,10 +583,9 @@ def main(): f"(止损{sl}(-{round((price_in-sl)/price_in*100,1)}%)" f"止盈{tp}(+{round(target_gain_pct,1)}%)" f" RR={rr})\n" - f" 理由:{sell_note}已深套,回本需{recover_str}不现实," - f"继续持有大概率继续亏(死钱);" - f"换仓到有信号票,止损可控(-3%)," - f"上行空间(+{round(target_gain_pct,1)}%)明确。" + f" 理由:{', '.join(sell_names)}前景评分最低," + f"继续持有无积极信号且技术偏弱;" + f"换到有明确信号和止损的标的,预期收益更优。" ) return text, gap @@ -598,7 +679,7 @@ def main(): if lots == 0: swap_text, _ = evaluate_swap( lot, rr, sig, tp, sl, name, code, price, - total_assets, available_cash, pf + total_assets, available_cash, pf, code_data ) lines.append(