fix: capital_flow_collector+per_stock_reassess归位
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
#!/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")
|
||||
from strategy_lifecycle import reassess_with_context as reassess_strategy
|
||||
|
||||
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
|
||||
with open(DECISIONS_PATH) as f:
|
||||
raw = json.load(f)
|
||||
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:
|
||||
print(f"[SKIP] {code}: 不在 decisions.json 中")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
# Always fetch live price for accurate reassessment
|
||||
price = 0
|
||||
try:
|
||||
import urllib.request
|
||||
code_raw = entry.get("code", "")
|
||||
sym_map = {"6":"sh","5":"sh","0":"sz","3":"sz"}
|
||||
prefix = ""
|
||||
for k, v in sym_map.items():
|
||||
if code_raw.startswith(k):
|
||||
prefix = v
|
||||
break
|
||||
if not prefix:
|
||||
prefix = "hk" if len(code_raw) == 5 else "sz"
|
||||
url = f"http://qt.gtimg.cn/q={prefix}{code_raw}"
|
||||
resp = urllib.request.urlopen(url, timeout=5)
|
||||
text = resp.read().decode("gbk")
|
||||
fields = text.split('"')[1].split("~")
|
||||
price = float(fields[3]) if fields[3] else 0
|
||||
# 港股:腾讯API返回HKD,统一转CNY
|
||||
if len(code_raw) == 5 and code_raw[0] in '01':
|
||||
try:
|
||||
sys.path.insert(0, '/home/hmo/MoFin')
|
||||
from hk_rate import hkd_to_cny
|
||||
_hkd_rate = hkd_to_cny()
|
||||
except Exception:
|
||||
_hkd_rate = 0.87
|
||||
price = round(price * _hkd_rate, 2)
|
||||
print(f" 实时价: {price} {'(CNY)' if len(code_raw) == 5 and code_raw[0] in '01' else ''}")
|
||||
except Exception as e:
|
||||
print(f" 实时价获取失败: {e}", file=sys.stderr)
|
||||
# Try portfolio.json as fallback (price_monitor keeps live prices)
|
||||
try:
|
||||
with open("/home/hmo/web-dashboard/data/portfolio.json") as _pf:
|
||||
_pf_data = json.load(_pf)
|
||||
for _h in _pf_data.get("holdings", []):
|
||||
if _h["code"] == code_raw:
|
||||
price = float(_h.get("price", 0))
|
||||
print(f" 从portfolio.json取实时价: {price}")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if price == 0:
|
||||
price = entry.get("current_price") or entry.get("price") or 0
|
||||
print(f" fallback到存储价: {price}", file=sys.stderr)
|
||||
|
||||
# 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()
|
||||
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,
|
||||
})
|
||||
# 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
|
||||
|
||||
# 写回 decisions.json(只更新被修改的那条,其余保留原样)
|
||||
raw["decisions"] = list(decisions_map.values())
|
||||
raw["total"] = len(raw["decisions"])
|
||||
from datetime import datetime
|
||||
raw["regenerated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
with open(DECISIONS_PATH, "w") as f:
|
||||
json.dump(raw, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"[DONE] {ok}成功 {skipped}跳过 {errors}失败")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user