Files
MoFin/scripts/per_stock_reassess.py
T
知微 2762ddf3ae fix: DB路径大面积修复——mofin_db/server/technical_analysis/strategy_tree等脚本的Path(__file__).parent/data错误指向scripts/data而非data/ 导致读写分离
- 量价分析: full_analysis输出volume_deep+成交量存储在price_history.json
- FK约束移除: holding_strategies外键->holdings阻止自选股写入
- #000850 重评已写入(止损3.74/止盈4.06/RR1.67)含量价信号
2026-07-08 12:34:39 +08:00

367 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
per_stock_reassess.py — 按个股触发重评
对每只传进来的 code 执行 reassess_strategy(),然后只更新
decisions.json 中对应的那一条记录。不碰 portfolio.json,不跑全量。
"""
import sys, json, os, re
sys.path.insert(0, "/home/hmo/web-dashboard")
sys.path.insert(0, "/home/hmo/MoFin")
from strategy_lifecycle import reassess_with_context as reassess_strategy
from mo_data import read_decisions, read_portfolio
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
def main():
codes = [a for a in sys.argv[1:] if not a.startswith("-")]
if not codes:
print("[FULL] 无指定编码,跑全量 regenerate_all()")
from strategy_lifecycle import regenerate_all
regenerate_all(stdout=False)
print("[FULL] 全量重评完成")
return
# 读现有 decisions
raw = read_decisions()
decisions_map = {d["code"]: d for d in raw.get("decisions", []) if d.get("code")}
ok = 0
errors = 0
skipped = 0
for code in codes:
entry = decisions_map.get(code)
if not entry:
# 可能是不在 decisions.json 的自选股 → 从 DB watchlist_stocks 构建entry
import sqlite3
_db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
_db.row_factory = sqlite3.Row
_wl = _db.execute("SELECT * FROM watchlist_stocks WHERE code=? AND is_active=1", (code,)).fetchone()
_db.close()
if _wl:
entry = {
"code": code,
"name": _wl["name"],
"price": _wl["price"] or 0,
"cost": 0,
"shares": 0,
"entry_low": _wl["entry_low"] or 0,
"entry_high": _wl["entry_high"] or 0,
"stop_loss": _wl["stop_loss"] or 0,
"take_profit": 0,
"action": "",
"type": "自选策略",
"is_watchlist": True,
"analysis": json.loads(_wl["analysis_json"]) if _wl["analysis_json"] else {}
}
print(f"[WL] {code} {_wl['name']}: 从自选表构建entry")
if not entry:
print(f"[SKIP] {code}: 不在 decisions.json 或 watchlist_stocks 中")
errors += 1
continue
try:
# Always fetch live price for accurate reassessment
price = 0
try:
# 价格从 DB 读取(price_monitor 每2分钟更新,唯一价格入口)
code_raw = entry.get("code", "")
price = 0
import sqlite3
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
db.row_factory = sqlite3.Row
row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (code_raw,)).fetchone()
if not row:
row = db.execute("SELECT price FROM watchlist_stocks WHERE code=? AND is_active=1", (code_raw,)).fetchone()
if not row:
row = db.execute("SELECT price FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (code_raw,)).fetchone()
if row:
price = row['price'] or 0
db.close()
if price > 0:
print(f" 实时价: {price} (来自DB)")
else:
# fallback to portfolio.json
_pf_data = read_portfolio()
for _h in _pf_data.get("holdings", []):
if _h["code"] == code_raw:
price = float(_h.get("price", 0))
break
if price <= 0:
price = entry.get("current_price") or entry.get("price") or 0
except Exception as e:
print(f" 价格获取失败: {e}", file=sys.stderr)
price = entry.get("current_price") or entry.get("price") or 0
# Price diff debounce: skip reassessment if price changed < 1% since last update
last_price = entry.get("last_reassessed_price", 0)
if last_price > 0 and price > 0:
diff_pct = abs(price - last_price) / last_price * 100
if diff_pct < 1.0:
print(f" 价差仅{diff_pct:.2f}% (<1%),跳过重评(上次价={last_price},现价={price}")
skipped += 1
continue
result = reassess_strategy(
code=code,
name=entry.get("name", ""),
price=price,
cost=entry.get("cost", 0),
shares=entry.get("shares", 0),
current_action=entry.get("action", ""),
is_watchlist=entry.get("type", "") in ("自选策略", "watchlist"),
)
if result and result.get("action"):
# 持仓股止损不下移(移动止损规则):已有仓位的止损只上不下
is_held = entry.get("cost", 0) > 0 and entry.get("shares", 0) > 0 and \
entry.get("type", "") not in ("自选策略", "watchlist")
old_stop = entry.get("stop_loss", 0)
new_stop = result.get("stop_loss", 0)
if is_held and old_stop > 0 and new_stop > 0 and new_stop < old_stop:
print(f" 移动止损保护: {new_stop}→保持{old_stop} (持仓止损不下移)")
result["stop_loss"] = old_stop
# 同时更新 action 字符串中的止损值
act = result.get("action", "")
if act:
act = re.sub(r'止损[\d.]+', f'止损{old_stop}', act)
result["action"] = act
# 更新 decisions_map 中对应的条目
updated = entry.copy()
# 币种标记:HK股保留HKD原始值,A股为CNY
is_hk = len(str(code)) == 5 and str(code)[0] in '01'
updated.update({
"action": result["action"],
"stop_loss": result.get("stop_loss", entry.get("stop_loss")),
"entry_low": result.get("entry_low", entry.get("entry_low")),
"entry_high": result.get("entry_high", entry.get("entry_high")),
"take_profit": result.get("take_profit"),
"tech_snapshot": result.get("tech_snapshot", entry.get("tech_snapshot")),
"timing_signal": result.get("timing_signal", entry.get("timing_signal")),
"rr_ratio": result.get("rr_ratio", entry.get("rr_ratio", 0)),
"status": result.get("status", "updated"),
"price": price,
"currency": "HKD" if is_hk else "CNY",
})
# Save last reassessed price for debounce tracking
updated["last_reassessed_price"] = price
decisions_map[code] = updated
# ——— 初始化多分支策略树 ———
try:
sys.path.insert(0, '/home/hmo/MoFin')
from strategy_tree import init_default_branches
branches = init_default_branches(
code,
entry.get('name', ''),
result.get('entry_low', 0),
result.get('entry_high', 0),
result.get('stop_loss', 0),
result.get('take_profit', 0),
)
st = updated.setdefault('strategy_tree', {})
st['branches'] = branches
except Exception:
pass
print(f"[OK] {code} {entry.get('name','')}: {result['action'][:80]}")
ok += 1
else:
print(f"[SYNCED] {code}: 无变更")
ok += 1
except Exception as e:
print(f"[ERROR] {code}: {e}", file=sys.stderr)
errors += 1
# 策略数据已通过DB写入(holding_strategies表),json.dump到decisions.json已废弃
# 同步自选股更新回 watchlist_stocks 表
try:
from datetime import datetime as _dt
import sqlite3
_db2 = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
for _code in codes:
_entry = decisions_map.get(_code)
if _entry and _entry.get("is_watchlist"):
_db2.execute("""
UPDATE watchlist_stocks
SET entry_low=?, entry_high=?, stop_loss=?, price=?,
analysis_json=json(?)
WHERE code=? AND is_active=1
""", (
_entry.get("entry_low", 0),
_entry.get("entry_high", 0),
_entry.get("stop_loss", 0),
_entry.get("price", 0),
json.dumps({
"action": _entry.get("action",""),
"take_profit": _entry.get("take_profit", 0),
"stop_loss": _entry.get("stop_loss", 0),
"tech_snapshot": _entry.get("tech_snapshot", ""),
"rr": _entry.get("rr_ratio", 0),
"reassessed_at": _dt.now().strftime("%Y-%m-%d")
}, ensure_ascii=False),
_code
))
_db2.commit()
_db2.close()
if any(e.get("is_watchlist") for e in [decisions_map.get(c) for c in codes] if e):
print("[SYNC] 自选股策略已同步回 watchlist_stocks 表")
except Exception as e:
print(f"[SYNC FAIL] watchlist_stocks 同步失败: {e}", file=sys.stderr)
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()