feat: add watchlist auto-reassessment to per_stock_reassess
- Add scan_watchlist_stocks() function that scans watchlist_stocks table - Deviation formula: max(|price-entry_low|,|price-entry_high|)/entry_low*100 > 20% - Calls technical_analysis.full_analysis() to get latest support/resistance - Updates entry_low/entry_high/stop_loss/price/analysis_json in DB - Records changelog in analysis_json with old/new values - Limits to 3 stocks per run, logs remaining=N for overflow - Scans watchlist AFTER decisions scan in main() (no changes to decisions logic)
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user