自选股自动重评机制+000850华茂股份全面重评
1. stale_detector新增自选股买入区偏离自动重评: - 每轮扫描watchlist_stocks, price偏离买入区中心>15%自动触发per_stock_reassess - 之前只标记[STRATEGY_STALE]不输出,改为标记+触发重评两步完成 - 策略完毕直接输出结果,不再等下次cron通知 2. 000850华茂股份全面重评: - 核心价值:纺织是壳,金融股权投资才是核心(国泰海通/广发/徽商银行) - PB=0.78破净, 7月3日分红3675万占年净利17.85% - 7/16临时股东会催化剂 - 结论:3.70~3.90区间可建仓1~2%,止损3.50,止盈4.30 - 修复:之前说'观望不建仓'是错的,低估了破净安全垫 3. watchlist_stocks DB加000850记录
This commit is contained in:
@@ -107,6 +107,51 @@ def main():
|
||||
print("[SILENT] 无需要检查的策略")
|
||||
return 0
|
||||
|
||||
# ----- 自选股买入区偏离自动重评 (2026-07-07 fix: 不只标记, 直接触发) -----
|
||||
try:
|
||||
import subprocess, sqlite3
|
||||
db = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
|
||||
db.row_factory = sqlite3.Row
|
||||
wl_stocks = db.execute(
|
||||
"SELECT code, name, price, entry_low, entry_high, analysis_json "
|
||||
"FROM watchlist_stocks WHERE is_active=1 AND entry_low IS NOT NULL"
|
||||
).fetchall()
|
||||
db.close()
|
||||
reassess_scripts = []
|
||||
for ws in wl_stocks:
|
||||
code, name, wl_price, wl_el, wl_eh, wl_aj = ws
|
||||
if not wl_el or not wl_el or wl_el <= 0:
|
||||
continue
|
||||
center = (wl_el + wl_eh) / 2
|
||||
# 从 decisions 拿实时价
|
||||
price_map = fetch_prices([code])
|
||||
cur_price = price_map.get(code, (None, None))[0]
|
||||
if not cur_price or cur_price <= 0:
|
||||
continue
|
||||
drift = (cur_price / center - 1) * 100
|
||||
if abs(drift) > 15:
|
||||
reassess_scripts.append(code)
|
||||
print(f"[AUTO_REASSESS] {name}({code}) 价{cur_price:.2f}偏离买入区中心{center:.2f} {drift:+.0f}% → 触发重评")
|
||||
if reassess_scripts:
|
||||
# 调用 per_stock_reassess
|
||||
reassess_path = None
|
||||
for p in ['/home/hmo/MoFin/scripts/per_stock_reassess.py',
|
||||
'/home/hmo/.hermes/profiles/position-analyst/scripts/per_stock_reassess.py']:
|
||||
if os.path.exists(p):
|
||||
reassess_path = p
|
||||
break
|
||||
if reassess_path:
|
||||
for code in reassess_scripts:
|
||||
r = subprocess.run(['python3', reassess_path, '--code', code],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
out = r.stdout.strip()[:200] if r.stdout else ""
|
||||
err = r.stderr.strip()[:200] if r.stderr else ""
|
||||
print(f" → {code}: exited={r.returncode} {out} {('err='+err) if err else ''}")
|
||||
print(f" → {code}: 重评完成")
|
||||
except Exception as e:
|
||||
print(f"[AUTO_REASSESS FAIL] {e}")
|
||||
# ----- 结束 自选股重评 -----
|
||||
|
||||
# ----- 组合级监测:读取总仓位 + 弱势比例 -----
|
||||
position_pct = 0
|
||||
cash = 0
|
||||
@@ -188,10 +233,12 @@ def main():
|
||||
issues.append(f"[PUSH] 价{price:.2f}入买入区{el}~{eh}")
|
||||
elif price > eh * 1.35:
|
||||
flags.append("[WL_HIGH]")
|
||||
issues.append(f"价{price:.2f}高出买入区+{((price/eh)-1)*100:.0f}%,买入区需重评")
|
||||
flags.append("[STRATEGY_STALE]")
|
||||
issues.append(f"[STRATEGY_STALE] 价{price:.2f}高出买入区+{((price/eh)-1)*100:.0f}%,买入区需重评")
|
||||
elif price > eh * 1.20:
|
||||
flags.append("[WL_DRIFT]")
|
||||
issues.append(f"价{price:.2f}高于买入区+{((price/eh)-1)*100:.0f}%")
|
||||
flags.append("[STRATEGY_STALE]")
|
||||
issues.append(f"[STRATEGY_STALE] 价{price:.2f}高出买入区+{((price/eh)-1)*100:.0f}%,买入区需重评")
|
||||
elif not is_wl and eh:
|
||||
dp = (price / eh - 1) * 100
|
||||
if dp > 35:
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成策略评估摘要"""
|
||||
import json
|
||||
|
||||
with open('/home/hmo/web-dashboard/data/decisions.json') as f:
|
||||
dec = json.load(f)
|
||||
with open('/home/hmo/MoFin/data/portfolio.json') as f:
|
||||
pf = json.load(f)
|
||||
|
||||
holdings = pf.get('holdings', [])
|
||||
cash = pf.get('cash', 321271)
|
||||
hk_rate = 0.867
|
||||
code_to_h = {h['code']: h for h in holdings}
|
||||
|
||||
decisions = dec.get('decisions', [])
|
||||
hold_entries = [s for s in decisions if s.get('shares', 0) > 0]
|
||||
wl_entries = [s for s in decisions if s.get('shares', 0) == 0]
|
||||
|
||||
hk_total_cny = 0
|
||||
a_total = 0
|
||||
for h in holdings:
|
||||
mv = h['shares'] * h['price']
|
||||
if h.get('currency') == 'HKD':
|
||||
hk_total_cny += mv * hk_rate
|
||||
else:
|
||||
a_total += mv
|
||||
total_mv = hk_total_cny + a_total
|
||||
total_assets = total_mv + cash
|
||||
position_pct = total_mv / total_assets * 100
|
||||
|
||||
weak_count = sum(1 for s in hold_entries if s.get('stock_category') in ('弱势','深套'))
|
||||
|
||||
print(f'总市值: {total_mv:.0f} CNY (HK${hk_total_cny:.0f} A¥{a_total:.0f})')
|
||||
print(f'总资产: {total_assets:.0f} CNY')
|
||||
print(f'仓位: {position_pct:.1f}% 现金: {cash:.0f}')
|
||||
print(f'持仓: {len(hold_entries)}只 弱势/深套: {weak_count}只 ({weak_count/len(hold_entries)*100:.0f}%)')
|
||||
print(f'自选: {len(wl_entries)}只')
|
||||
print()
|
||||
|
||||
print('【持仓详情】')
|
||||
for s in hold_entries:
|
||||
code = s['code']
|
||||
name = s['name']
|
||||
shares = s['shares']
|
||||
cost = s.get('cost', 0)
|
||||
sl = s.get('stop_loss', 0)
|
||||
tp = s.get('take_profit', 0)
|
||||
cat = s.get('stock_category', '?')
|
||||
sig = s.get('timing_signal', '?')
|
||||
|
||||
h = code_to_h.get(code)
|
||||
price = h['price'] if h else 0
|
||||
if h and h.get('currency') == 'HKD':
|
||||
mv_val = h['shares'] * h['price'] * hk_rate
|
||||
else:
|
||||
mv_val = h['shares'] * h['price'] if h else 0
|
||||
pl_pct = (price - cost) / cost * 100 if cost else 0
|
||||
pct = mv_val / total_assets * 100
|
||||
sl_dist = (price / sl - 1) * 100 if sl > 0 else 0
|
||||
tp_dist = (tp / price - 1) * 100 if tp > 0 else 0
|
||||
|
||||
flags = []
|
||||
if sl_dist < 5:
|
||||
if pl_pct > 5:
|
||||
flags.append('利润保护')
|
||||
else:
|
||||
flags.append(f'⚠️近止损({sl_dist:.0f}%)')
|
||||
if tp_dist < 5 and tp_dist > 0:
|
||||
flags.append('近止盈')
|
||||
if cat in ('弱势','深套'):
|
||||
flags.append(f'[{cat}]')
|
||||
|
||||
flag_str = ' '.join(flags) if flags else ''
|
||||
print(f' {code} {name:10s} ¥{price:>7.2f} 浮{pl_pct:+.1f}% 仓{pct:.1f}% 损{sl}({sl_dist:.0f}%) 盈{tp}({tp_dist:.0f}%) {flag_str}')
|
||||
|
||||
print()
|
||||
print('【自选关注】')
|
||||
for s in wl_entries:
|
||||
code = s['code']
|
||||
name = s['name']
|
||||
el = s.get('entry_low', 0)
|
||||
eh = s.get('entry_high', 0)
|
||||
sl = s.get('stop_loss', 0)
|
||||
price = s.get('price', 0)
|
||||
sig = s.get('timing_signal', '?')
|
||||
in_zone = '✅在买入区' if el and eh and price and el <= price <= eh else ''
|
||||
print(f' {code} {name:10s} ¥{price:>7.2f} 买区{el}~{eh} 损{sl} 信号{sig} {in_zone}')
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""将 decisions.json 全量同步到 SQLite holding_strategies 表"""
|
||||
import json, sqlite3, sys
|
||||
|
||||
DECISIONS_PATH = '/home/hmo/web-dashboard/data/decisions.json'
|
||||
DB_PATH = '/home/hmo/web-dashboard/data/mofin.db'
|
||||
|
||||
def main():
|
||||
# 读 decisions.json
|
||||
with open(DECISIONS_PATH) as f:
|
||||
data = json.load(f)
|
||||
entries = data.get('decisions', [])
|
||||
print(f'Read {len(entries)} entries from decisions.json')
|
||||
|
||||
db = sqlite3.connect(DB_PATH)
|
||||
|
||||
# 先清空 holding_strategies(全量重建更干净)
|
||||
db.execute('DELETE FROM holding_strategies')
|
||||
|
||||
inserted = 0
|
||||
for d in entries:
|
||||
code = d.get('code')
|
||||
if not code:
|
||||
continue
|
||||
|
||||
# 从 decisions.json 提取字段,映射到 DB schema
|
||||
sql = '''INSERT INTO holding_strategies (
|
||||
code, name, version, price, cost, shares,
|
||||
stop_loss, take_profit, entry_low, entry_high,
|
||||
currency, strategy_type, action, timing_signal,
|
||||
rr_ratio, tech_snapshot, stock_category, sector_context,
|
||||
status, trigger_json, changelog_json, source, reason,
|
||||
created_at, updated_at,
|
||||
avg_price, decision_timestamp, note, decision_type
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'''
|
||||
|
||||
# 确定 type/strategy_type
|
||||
stype = d.get('strategy_type') or d.get('type') or '持仓策略'
|
||||
# decision_type = d.get('decision_type') or stype
|
||||
decision_type = stype
|
||||
|
||||
vals = (
|
||||
code,
|
||||
d.get('name', ''),
|
||||
d.get('version', 1),
|
||||
d.get('price'),
|
||||
d.get('cost'),
|
||||
d.get('shares', 0),
|
||||
d.get('stop_loss'),
|
||||
d.get('take_profit'),
|
||||
d.get('entry_low'),
|
||||
d.get('entry_high'),
|
||||
d.get('currency', 'CNY' if code.startswith(('6','0','3','5')) else 'HKD'),
|
||||
stype,
|
||||
d.get('action', ''),
|
||||
d.get('timing_signal', ''),
|
||||
d.get('rr_ratio'),
|
||||
d.get('tech_snapshot'),
|
||||
d.get('stock_category'),
|
||||
d.get('sector_context'),
|
||||
d.get('status', 'active'),
|
||||
json.dumps(d.get('trigger', {}), ensure_ascii=False) if d.get('trigger') else d.get('trigger_json'),
|
||||
json.dumps(d.get('changelog', []), ensure_ascii=False) if d.get('changelog') else d.get('changelog_json'),
|
||||
d.get('source', 'auto'),
|
||||
d.get('reason'),
|
||||
d.get('created_at'),
|
||||
d.get('updated_at'),
|
||||
d.get('avg_price'),
|
||||
d.get('decision_timestamp') or d.get('timestamp'),
|
||||
d.get('note'),
|
||||
decision_type,
|
||||
)
|
||||
try:
|
||||
db.execute(sql, vals)
|
||||
inserted += 1
|
||||
except Exception as e:
|
||||
print(f'Error inserting {code} ({d.get("name")}): {e}')
|
||||
|
||||
db.commit()
|
||||
|
||||
# 验证
|
||||
cnt = db.execute('SELECT COUNT(*) FROM holding_strategies').fetchone()[0]
|
||||
active = db.execute('SELECT COUNT(*) FROM holding_strategies WHERE status IN ("active","updated")').fetchone()[0]
|
||||
db.close()
|
||||
|
||||
print(f'Synced: {inserted} rows inserted')
|
||||
print(f'holding_strategies: {cnt} total, {active} active/updated')
|
||||
|
||||
return 0 if inserted > 0 else 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user