diff --git a/scripts/per_stock_reassess.py b/scripts/per_stock_reassess.py index a317fa93..cee0bd6c 100644 --- a/scripts/per_stock_reassess.py +++ b/scripts/per_stock_reassess.py @@ -216,6 +216,157 @@ def main(): print(f"[DONE] {ok}成功 {skipped}跳过 {errors}失败") + # ── 第二步:扫描自选股(watchlist),价格偏离买入区>20%触发重评 ── + scan_watchlist_stocks() + + +# ════════════════════════════════════════════════════════════════════ +# 自选股扫描 +# ════════════════════════════════════════════════════════════════════ + +def scan_watchlist_stocks(): + """扫描自选股表 (watchlist_stocks),对价格偏离买入区 >20% 的股票自动重评。 + + 偏离公式: max(|price - entry_low|, |price - entry_high|) / entry_low * 100 > 20 + + 通过 technical_analysis.full_analysis() 获取最新支撑/阻力位, + 更新 entry_low / entry_high / stop_loss / price / analysis_json。 + 每轮最多处理 3 只,超过时标记剩余数量待下次扫描。 + """ + import sqlite3, json + from datetime import datetime + from technical_analysis import full_analysis + from mo_models import is_hk_stock + + DB = '/home/hmo/web-dashboard/data/mofin.db' + db = sqlite3.connect(DB) + db.row_factory = sqlite3.Row + + rows = db.execute( + "SELECT * FROM watchlist_stocks WHERE is_active=1" + ).fetchall() + + if not rows: + print("[WL-SCAN] 自选股表为空,跳过") + db.close() + return + + # ── 筛选偏离 >20% 的股票 ── + candidates = [] # (code, name, price, entry_low, entry_high, stop_loss, deviation, analysis_json) + for r in rows: + code = r["code"] + name = r["name"] + price = r["price"] or 0 + entry_low = r["entry_low"] or 0 + entry_high = r["entry_high"] or 0 + stop_loss = r["stop_loss"] or 0 + analysis_json = r["analysis_json"] + + if entry_low <= 0 or price <= 0: + continue + + dev_low = abs(price - entry_low) + dev_high = abs(price - entry_high) + deviation = max(dev_low, dev_high) / entry_low * 100 + + if deviation > 20: + candidates.append((code, name, price, entry_low, entry_high, stop_loss, deviation, analysis_json)) + + total_needed = len(candidates) + print(f"[WL-SCAN] 自选股共{len(rows)}只,偏离>20%需重评: {total_needed}只") + + MAX_PER_RUN = 3 + to_process = candidates[:MAX_PER_RUN] + remaining = max(0, total_needed - MAX_PER_RUN) + + if remaining > 0: + print(f"[WL-SCAN] 本轮限{MAX_PER_RUN}只,剩余{remaining}只待下次扫描") + + if not to_process: + print("[WL-SCAN] 无需重评") + db.close() + return + + ok = 0 + errors = 0 + for code, name, price, old_low, old_high, old_stop, deviation, old_analysis_json in to_process: + print(f"[WL-REASSESS] {code} {name}: 偏离{deviation:.1f}%,触发重评") + try: + ta = full_analysis(code) + if not ta or "error" in ta: + print(f" [WARN] TA失败: {ta}") + errors += 1 + continue + + sr = ta.get("support_resistance", {}) + if "error" in sr: + print(f" [WARN] 支撑/阻力计算失败: {sr}") + errors += 1 + continue + + new_price = ta.get("quote", {}).get("price", price) + new_entry_low = round(sr.get("weak_support", old_low), 2) + new_entry_high = round(sr.get("weak_resist", old_high), 2) + new_stop_loss = round(sr.get("strong_support", old_stop), 2) + new_take_profit = round(sr.get("strong_resist", 0), 2) + + # ── 更新 analysis_json + changelog ── + old_analysis = json.loads(old_analysis_json) if old_analysis_json else {} + changelog = old_analysis.get("changelog", []) + changelog.append({ + "action": "auto_reassess_watchlist", + "reason": f"价格偏离买入区{deviation:.1f}%", + "old_entry_low": old_low, + "old_entry_high": old_high, + "new_entry_low": new_entry_low, + "new_entry_high": new_entry_high, + "old_stop_loss": old_stop, + "new_stop_loss": new_stop_loss, + "take_profit": new_take_profit, + "price": new_price, + "deviation_pct": round(deviation, 1), + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"), + }) + + new_analysis = { + **old_analysis, + "take_profit": new_take_profit, + "tech_snapshot": { + "support_resistance": sr, + "candlestick": ta.get("candlestick", {}), + "volume": ta.get("volume", {}), + "analyzed_at": ta.get("analyzed_at", ""), + }, + "reassessed_at": datetime.now().strftime("%Y-%m-%d"), + "changelog": changelog, + } + + currency = "HKD" if is_hk_stock(code) else "CNY" + + db.execute(""" + UPDATE watchlist_stocks + SET entry_low=?, entry_high=?, stop_loss=?, price=?, + currency=?, analysis_json=? + WHERE code=? AND is_active=1 + """, ( + new_entry_low, new_entry_high, new_stop_loss, + new_price, currency, json.dumps(new_analysis, ensure_ascii=False), + code, + )) + db.commit() + print(f" [OK] {code} {name}: 买入区{old_low}-{old_high} -> {new_entry_low}-{new_entry_high}, " + f"止损{new_stop_loss}, 止盈{new_take_profit}") + ok += 1 + except Exception as e: + import traceback + print(f" [ERROR] {code}: {e}", file=sys.stderr) + traceback.print_exc(file=sys.stderr) + errors += 1 + + db.close() + remaining_msg = f" (剩余{remaining}只)" if remaining else "" + print(f"[WL-SCAN] DONE: {ok}成功 {errors}失败{remaining_msg}") + if __name__ == "__main__": main()