From 99cce27a2cfd24f27806330b4bf7678f2e9cca1d Mon Sep 17 00:00:00 2001 From: xxm Date: Thu, 20 Aug 2026 22:54:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(C=E7=BB=84):=20clean=5Fwatchlist?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=87=BD=E6=95=B0+import=5Fholding=5Fxls?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=B0=83=E7=94=A8+premarket=5Ffull=5Freview?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=85=9C=E5=BA=95=E5=AF=B9=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/clean_watchlist.py | 257 +++++------ deploy/profile-scripts/import_holding_xls.py | 402 +++++++++--------- .../profile-scripts/premarket_full_review.py | 11 + 3 files changed, 344 insertions(+), 326 deletions(-) diff --git a/deploy/profile-scripts/clean_watchlist.py b/deploy/profile-scripts/clean_watchlist.py index bf24afa5..51dc8747 100644 --- a/deploy/profile-scripts/clean_watchlist.py +++ b/deploy/profile-scripts/clean_watchlist.py @@ -1,125 +1,132 @@ -#!/usr/bin/env python3 -"""Remove held stocks from watchlist""" - -import json, os, sys - -# 确保 MoFin 根目录在模块搜索路径中(兼容 cron 环境) -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from mo_data import read_portfolio, read_decisions, read_watchlist -from mofin_db import get_conn, write_watchlist_stock, write_holding_strategy - -WL = "/home/hmo/web-dashboard/data/watchlist.json" # 路径保留用于历史备份兼容,数据实际走DB -# 决策数据全部从DB读取,json文件已移除 -DEC = "/home/hmo/web-dashboard/data/decisions.json" # 保留常量但不再使用,防止引用报错 - -holding_codes = set() -pf = read_portfolio() -for h in pf.get("holdings", []): - c = h.get("code", "") - if c: - holding_codes.add(c) - -print(f"持仓 codes: {sorted(holding_codes)}") - -# Load watchlist -wl = read_watchlist() -stocks = wl.get("stocks", []) -before = len(stocks) - -# Remove held stocks -new_stocks = [s for s in stocks if s.get("code") not in holding_codes] -removed = [s for s in stocks if s.get("code") in holding_codes] - -after = len(new_stocks) -wl["stocks"] = new_stocks - -# Backup — DB 版,不再碰JSON文件 -# DB 写入 -conn = get_conn() -for s in wl.get("stocks", []): - s.setdefault("currency", "CNY") - write_watchlist_stock(conn, s) -conn.close() -# [migrated to DB] — cold backup removed -# json.dump(wl, open(WL, "w"), indent=2, ensure_ascii=False) - -print(f"\n自选: {before} → {after} 只") -print(f"移除 {len(removed)} 只:") -for r in removed: - print(f" {r['code']} {r.get('name','')}") - -# Also update decisions.json - set them to "managed_by_holdings" or remove watchlist-only fields -dec = read_decisions() -dec_changed = 0 -for d in dec.get("decisions", []): - code = d.get("code", "") - if code in [r["code"] for r in removed]: - # Remove watchlist-specific tags - if d.get("tag") == "watchlist": - d["tag"] = "managed_by_holdings" - dec_changed += 1 - -if dec_changed: - # DB 写入(不再碰JSON文件) - conn = get_conn() - for d in dec.get("decisions", []): - write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) - conn.close() - # [migrated to DB] — cold backup removed - # json.dump(dec, open(DEC, "w"), indent=2, ensure_ascii=False) - print(f"\ndecisions数据: {dec_changed} 只更新标签") -else: - print(f"\ndecisions数据: 无需更新") - -# ── 反过程:清仓股自动加回自选 ── -# 找出曾持仓但现已不在 portfolio 的股票 -prev_held = {} # code → last_execution info -for d in dec.get("decisions", []): - code = d.get("code", "") - exec_info = d.get("execution", {}) - if exec_info and exec_info.get("status") in ("executing", "partial_exit"): - # 当前仍持仓但不在 portfolio?说明 portfolio 数据落后,跳过 - pass - elif exec_info and exec_info.get("status") in ("sold", "closed") and code not in holding_codes: - prev_held[code] = { - "name": d.get("name", code), - "entry_low": d.get("entry_low", 0), - "entry_high": d.get("entry_high", 0), - } - -if prev_held: - wl_stock_codes = set(s.get("code", "") for s in new_stocks) - added = 0 - for code, info in sorted(prev_held.items()): - if code not in wl_stock_codes and code not in holding_codes: - # 确保买入区有值 - entry_low = info.get("entry_low", 0) or 0 - entry_high = info.get("entry_high", 0) or 0 - new_stocks.append({ - "code": code, - "name": info["name"], - "entry_low": entry_low, - "entry_high": entry_high, - "stop_loss": 0, - "tag": "recovered_from_sold", - }) - added += 1 - print(f" ← 已清仓→加回自选: {code} {info['name']}") - if added: - wl["stocks"] = new_stocks - # DB 写入 - conn = get_conn() - for s in wl.get("stocks", []): - s.setdefault("currency", "CNY") - write_watchlist_stock(conn, s) - conn.close() - # [migrated to DB] — cold backup removed - # json.dump(wl, open(WL, "w"), indent=2, ensure_ascii=False) - print(f"\n反过程: {added} 只清仓股已加回自选") - else: - print("\n反过程: 无清仓股需加回") -else: - print("\n反过程: 无已清仓记录") - -print("\nDONE") +#!/usr/bin/env python3 +"""Remove held stocks from watchlist""" + +import json, os, sys + +# 确保 MoFin 根目录在模块搜索路径中(兼容 cron 环境) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from mo_data import read_portfolio, read_decisions, read_watchlist +from mofin_db import get_conn, write_watchlist_stock, write_holding_strategy + +WL = "/home/hmo/web-dashboard/data/watchlist.json" # 路径保留用于历史备份兼容,数据实际走DB +# 决策数据全部从DB读取,json文件已移除 +DEC = "/home/hmo/web-dashboard/data/decisions.json" # 保留常量但不再使用,防止引用报错 + + +def main(): + holding_codes = set() + pf = read_portfolio() + for h in pf.get("holdings", []): + c = h.get("code", "") + if c: + holding_codes.add(c) + + print(f"持仓 codes: {sorted(holding_codes)}") + +# Load watchlist + wl = read_watchlist() +stocks = wl.get("stocks", []) + before = len(stocks) + +# Remove held stocks + new_stocks = [s for s in stocks if s.get("code") not in holding_codes] + removed = [s for s in stocks if s.get("code") in holding_codes] + + after = len(new_stocks) +wl["stocks"] = new_stocks + +# Backup — DB 版,不再碰JSON文件 +# DB 写入 + conn = get_conn() +for s in wl.get("stocks", []): + s.setdefault("currency", "CNY") + write_watchlist_stock(conn, s) + conn.close() +# [migrated to DB] — cold backup removed +# json.dump(wl, open(WL, "w"), indent=2, ensure_ascii=False) + + print(f"\n自选: {before} → {after} 只") + print(f"移除 {len(removed)} 只:") +for r in removed: + print(f" {r['code']} {r.get('name','')}") + +# Also update decisions.json - set them to "managed_by_holdings" or remove watchlist-only fields + dec = read_decisions() + dec_changed = 0 +for d in dec.get("decisions", []): + code = d.get("code", "") + if code in [r["code"] for r in removed]: + # Remove watchlist-specific tags + if d.get("tag") == "watchlist": + d["tag"] = "managed_by_holdings" + dec_changed += 1 + +if dec_changed: + # DB 写入(不再碰JSON文件) + conn = get_conn() + for d in dec.get("decisions", []): + write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d) + conn.close() + # [migrated to DB] — cold backup removed + # json.dump(dec, open(DEC, "w"), indent=2, ensure_ascii=False) + print(f"\ndecisions数据: {dec_changed} 只更新标签") +else: + print(f"\ndecisions数据: 无需更新") + +# ── 反过程:清仓股自动加回自选 ── +# 找出曾持仓但现已不在 portfolio 的股票 + prev_held = {} # code → last_execution info +for d in dec.get("decisions", []): + code = d.get("code", "") + exec_info = d.get("execution", {}) + if exec_info and exec_info.get("status") in ("executing", "partial_exit"): + # 当前仍持仓但不在 portfolio?说明 portfolio 数据落后,跳过 + pass + elif exec_info and exec_info.get("status") in ("sold", "closed") and code not in holding_codes: + prev_held[code] = { + "name": d.get("name", code), + "entry_low": d.get("entry_low", 0), + "entry_high": d.get("entry_high", 0), + } + +if prev_held: + wl_stock_codes = set(s.get("code", "") for s in new_stocks) + added = 0 + for code, info in sorted(prev_held.items()): + if code not in wl_stock_codes and code not in holding_codes: + # 确保买入区有值 + entry_low = info.get("entry_low", 0) or 0 + entry_high = info.get("entry_high", 0) or 0 + new_stocks.append({ + "code": code, + "name": info["name"], + "entry_low": entry_low, + "entry_high": entry_high, + "stop_loss": 0, + "tag": "recovered_from_sold", + }) + added += 1 + print(f" ← 已清仓→加回自选: {code} {info['name']}") + if added: + wl["stocks"] = new_stocks + # DB 写入 + conn = get_conn() + for s in wl.get("stocks", []): + s.setdefault("currency", "CNY") + write_watchlist_stock(conn, s) + conn.close() + # [migrated to DB] — cold backup removed + # json.dump(wl, open(WL, "w"), indent=2, ensure_ascii=False) + print(f"\n反过程: {added} 只清仓股已加回自选") + else: + print("\n反过程: 无清仓股需加回") +else: + print("\n反过程: 无已清仓记录") + + print("\nDONE") + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy/profile-scripts/import_holding_xls.py b/deploy/profile-scripts/import_holding_xls.py index f15975e9..0da38c2e 100644 --- a/deploy/profile-scripts/import_holding_xls.py +++ b/deploy/profile-scripts/import_holding_xls.py @@ -1,201 +1,201 @@ -#!/usr/bin/env python3 -""" -import_holding_xls.py — 从 holding.xls 导入持仓到全系统 - -用法: - python3 import_holding_xls.py [--cash 现金] [--total 总资产] [--mv 市值] - ---cash 必传!holding文件不含现金行,不传则现金=0。 - -不传 --total/--mv 则从 holding.xls 计算(可能有价格时差误差)。 -建议传截图上的真实数字。 - -示例: - python3 import_holding_xls.py --cash 73758.0 --total 874598.90 --mv 800840.90 -""" -import csv, json, sys, subprocess, sqlite3, os -from datetime import datetime -from mo_data import read_decisions -from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy - -STOCKS_FILE = "/home/hmo/stocks/holding.xls" -DB_PATH = "/home/hmo/web-dashboard/data/mofin.db" - - -def clean_cell(v): - v = v.strip() - if v.startswith('="') and v.endswith('"'): - v = v[2:-1] - elif v.startswith('='): - v = v[1:] - return v.strip() - - -def main(): - # Parse args - # ⚠️ 现金不从holding文件读取。holding只有股票持仓,现金必须单独提供(截图)。 - # 不传 --cash 则默认为0,会在后面警告。 - cash = 0.0 - total_assets = 0 - market_value = 0 - frozen_cash = 0.0 - - args = sys.argv[1:] - for i, a in enumerate(args): - if a == '--cash' and i + 1 < len(args): - cash = float(args[i + 1]) - elif a == '--total' and i + 1 < len(args): - total_assets = float(args[i + 1]) - elif a == '--mv' and i + 1 < len(args): - market_value = float(args[i + 1]) - elif a == '--frozen' and i + 1 < len(args): - frozen_cash = float(args[i + 1]) - - with open(STOCKS_FILE, 'r', encoding='gbk') as f: - reader = csv.reader(f, delimiter='\t') - rows = list(reader) - - print(f"读取 {STOCKS_FILE}: {len(rows)-1} 条记录") - - holdings = [] - total_mv_cny = 0 - - for r in rows[1:]: - code = clean_cell(r[0]) - name = r[1].strip() - shares = int(clean_cell(r[2])) - # 跳过0股(已清仓的残留条目) - if shares <= 0: - continue - price_raw = r[4].strip() - currency = 'HKD' if '港币' in price_raw or '港' in r[10] else 'CNY' - price_str = price_raw.replace('港币', '').replace('港元', '').replace('港', '').strip() - price = float(price_str) - cost_price_raw = float(clean_cell(r[5])) - pl = float(clean_cell(r[6])) if r[6].strip() else 0 - mkt_val_raw = float(clean_cell(r[11])) - cost_amount_raw = float(clean_cell(r[15])) if r[15].strip() and r[15].strip() != '--' else 0 - rate_str = clean_cell(r[16]) - rate = float(rate_str) if rate_str and rate_str != '--' else 0.8664 - - # 港股:个股一律存原币(HKD),不做 CNY 折算(v3 币种规范)。 - # mv 存 HKD 原值,汇总折算由 mo_models.calc_total_mv 统一负责。 - if currency == 'HKD': - cost_price = round(cost_price_raw, 2) - mv_cny = round(mkt_val_raw, 2) - price_cny = round(price, 2) - else: - cost_price = round(cost_price_raw, 2) - mv_cny = mkt_val_raw - price_cny = price - # total_mv_cny 仅用于 --total/--mv 未传时的回退汇总:A股直接累计,港股按汇率粗算成 CNY - total_mv_cny += mv_cny if currency == 'CNY' else round(mkt_val_raw * rate, 2) - - holdings.append({ - 'code': code, 'name': name, 'shares': shares, - 'price': price_cny, 'cost_price': cost_price, - 'currency': currency, 'market_val': mv_cny, - 'cost_amount_raw': cost_amount_raw, 'exchange_rate': rate, - }) - - if cash <= 0: - print("⚠️ 警告:未提供现金(--cash),现金默认=0。Dad可能给了截图现金数!") - print(" holding文件不含现金行,必须手动提供。可以用:") - print(f" python3 import_holding_xls.py --cash 73758.0") - - # Use provided values or calculate (unified formula includes frozen_cash) - if total_assets <= 0: - total_assets = total_mv_cny + cash + frozen_cash - if market_value <= 0: - market_value = round(total_mv_cny, 2) - - position_pct = round(market_value / total_assets * 100, 2) if total_assets > 0 else 0 - - # Step 1: Update SQLite (regenerate_all reads from here) - print("\n→ 更新 SQLite holdings 表...") - conn = sqlite3.connect(DB_PATH) - c = conn.cursor() - c.execute('DELETE FROM holdings') - c.execute('DELETE FROM portfolio_summary') - for h in holdings: - c.execute(''' - INSERT INTO holdings (code, name, shares, cost, currency, position_pct, added_at, is_active) - VALUES (?, ?, ?, ?, ?, ?, ?, 1) - ''', (h['code'], h['name'], h['shares'], h['cost_price'], h['currency'], - round(h['market_val'] / total_assets * 100, 2), - datetime.now().strftime('%Y-%m-%d'))) - c.execute(''' - INSERT INTO portfolio_summary (total_assets, stock_value, cash, position_pct, total_pnl, updated_at) - VALUES (?, ?, ?, ?, ?, ?) - ''', (round(total_assets, 2), round(market_value, 2), cash, - position_pct, 0, datetime.now().strftime('%Y-%m-%d %H:%M'))) - conn.commit() - conn.close() - print(f" OK - {len(holdings)} 只持仓") - - # Step 2: Run full reassessment (reads SQLite, writes decisions.json + portfolio.json) - print("\n→ 全量策略重评...") - subprocess.run( - ["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/per_stock_reassess.py"], - capture_output=True, text=True, timeout=120 - ) - print(f" 完成") - - # Step 3: Overwrite portfolio.json with correct aggregate numbers - # (regenerate_all writes its own format, we fix it back) - print("\n→ 修正 portfolio.json 汇总数据...") - portfolio = { - 'holdings': holdings, - 'cash': cash, - 'total_market_value': market_value, - 'total_assets': round(total_assets, 2), - 'total_pl': 0, - 'position_pct': position_pct, - 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M'), - 'source': STOCKS_FILE, - } - # DB 写入 - try: - conn = get_conn() - write_holdings_batch(conn, portfolio.get('holdings', [])) - write_portfolio_summary(conn, portfolio) - conn.close() - except Exception as e: - print(f" [DB写入失败] {e}") - - # Step 4: Rebuild decision trees - print("\n→ 重建决策树...") - sys.path.insert(0, '/home/hmo/web-dashboard') - from strategy_tree import init_default_branches - data = read_decisions() - ok = 0 - for e in data.get('decisions', []): - branches = init_default_branches( - e.get('code', ''), e.get('name', ''), - e.get('entry_low', 0), e.get('entry_high', 0), - e.get('stop_loss', 0), e.get('take_profit', 0)) - e['strategy_tree'] = {'branches': branches, 'created_at': datetime.now().strftime('%Y-%m-%d')} - ok += 1 - # DB 写入(替代 json.dump) - try: - conn = get_conn() - for d in data.get('decisions', []): - _whs(conn, d.get('code', ''), d.get('name', ''), d) - conn.close() - except Exception: - pass - # [migrated to DB] — cold backup removed - # with open('/home/hmo/web-dashboard/data/decisions.json', 'w') as f: - # json.dump(data, f, indent=2, ensure_ascii=False) - - print(f"\n{'='*50}") - print(f"导入完成:{len(holdings)}只持仓") - print(f"总资产: {round(total_assets):,.0f}元") - print(f"市值: {market_value:,.0f}元") - print(f"现金: {cash:,.0f}元") - print(f"仓位: {position_pct}%") - print(f"决策树: {ok}/{len(data.get('decisions',[]))}") - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +""" +import_holding_xls.py — 从 holding.xls 导入持仓到全系统 + +用法: + python3 import_holding_xls.py [--cash 现金] [--total 总资产] [--mv 市值] + +--cash 必传!holding文件不含现金行,不传则现金=0。 + +不传 --total/--mv 则从 holding.xls 计算(可能有价格时差误差)。 +建议传截图上的真实数字。 + +示例: + python3 import_holding_xls.py --cash 73758.0 --total 874598.90 --mv 800840.90 +""" +import csv, json, sys, subprocess, sqlite3, os +from datetime import datetime +from mo_data import read_decisions +from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy + +STOCKS_FILE = "/home/hmo/stocks/holding.xls" +DB_PATH = "/home/hmo/web-dashboard/data/mofin.db" + + +def clean_cell(v): + v = v.strip() + if v.startswith('="') and v.endswith('"'): + v = v[2:-1] + elif v.startswith('='): + v = v[1:] + return v.strip() + + +def main(): + # Parse args + # ⚠️ 现金不从holding文件读取。holding只有股票持仓,现金必须单独提供(截图)。 + # 不传 --cash 则默认为0,会在后面警告。 + cash = 0.0 + total_assets = 0 + market_value = 0 + frozen_cash = 0.0 + + args = sys.argv[1:] + for i, a in enumerate(args): + if a == '--cash' and i + 1 < len(args): + cash = float(args[i + 1]) + elif a == '--total' and i + 1 < len(args): + total_assets = float(args[i + 1]) + elif a == '--mv' and i + 1 < len(args): + market_value = float(args[i + 1]) + elif a == '--frozen' and i + 1 < len(args): + frozen_cash = float(args[i + 1]) + + with open(STOCKS_FILE, 'r', encoding='gbk') as f: + reader = csv.reader(f, delimiter='\t') + rows = list(reader) + + print(f"读取 {STOCKS_FILE}: {len(rows)-1} 条记录") + + holdings = [] + total_mv_cny = 0 + + for r in rows[1:]: + code = clean_cell(r[0]) + name = r[1].strip() + shares = int(clean_cell(r[2])) + # 跳过0股(已清仓的残留条目) + if shares <= 0: + continue + price_raw = r[4].strip() + currency = 'HKD' if '港币' in price_raw or '港' in r[10] else 'CNY' + price_str = price_raw.replace('港币', '').replace('港元', '').replace('港', '').strip() + price = float(price_str) + cost_price_raw = float(clean_cell(r[5])) + pl = float(clean_cell(r[6])) if r[6].strip() else 0 + mkt_val_raw = float(clean_cell(r[11])) + cost_amount_raw = float(clean_cell(r[15])) if r[15].strip() and r[15].strip() != '--' else 0 + rate_str = clean_cell(r[16]) + rate = float(rate_str) if rate_str and rate_str != '--' else 0.8664 + + # 港股:个股一律存原币(HKD),不做 CNY 折算(v3 币种规范)。 + # mv 存 HKD 原值,汇总折算由 mo_models.calc_total_mv 统一负责。 + if currency == 'HKD': + cost_price = round(cost_price_raw, 2) + mv_cny = round(mkt_val_raw, 2) + price_cny = round(price, 2) + else: + cost_price = round(cost_price_raw, 2) + mv_cny = mkt_val_raw + price_cny = price + # total_mv_cny 仅用于 --total/--mv 未传时的回退汇总:A股直接累计,港股按汇率粗算成 CNY + total_mv_cny += mv_cny if currency == 'CNY' else round(mkt_val_raw * rate, 2) + + holdings.append({ + 'code': code, 'name': name, 'shares': shares, + 'price': price_cny, 'cost_price': cost_price, + 'currency': currency, 'market_val': mv_cny, + 'cost_amount_raw': cost_amount_raw, 'exchange_rate': rate, + }) + + if cash <= 0: + print("⚠️ 警告:未提供现金(--cash),现金默认=0。Dad可能给了截图现金数!") + print(" holding文件不含现金行,必须手动提供。可以用:") + print(f" python3 import_holding_xls.py --cash 73758.0") + + # Use provided values or calculate (unified formula includes frozen_cash) + if total_assets <= 0: + total_assets = total_mv_cny + cash + frozen_cash + if market_value <= 0: + market_value = round(total_mv_cny, 2) + + position_pct = round(market_value / total_assets * 100, 2) if total_assets > 0 else 0 + + # Step 1: Update SQLite (regenerate_all reads from here) + print("\n→ 更新 SQLite holdings 表...") + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute('DELETE FROM holdings') + c.execute('DELETE FROM portfolio_summary') + for h in holdings: + c.execute(''' + INSERT INTO holdings (code, name, shares, cost, currency, position_pct, added_at, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, 1) + ''', (h['code'], h['name'], h['shares'], h['cost_price'], h['currency'], + round(h['market_val'] / total_assets * 100, 2), + datetime.now().strftime('%Y-%m-%d'))) + c.execute(''' + INSERT INTO portfolio_summary (total_assets, stock_value, cash, position_pct, total_pnl, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ''', (round(total_assets, 2), round(market_value, 2), cash, + position_pct, 0, datetime.now().strftime('%Y-%m-%d %H:%M'))) + conn.commit() + conn.close() + print(f" OK - {len(holdings)} 只持仓") + + # Step 2: Run full reassessment (reads SQLite, writes decisions.json + portfolio.json) + print("\n→ 全量策略重评...") + subprocess.run( + ["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/per_stock_reassess.py"], + capture_output=True, text=True, timeout=120 + ) + print(f" 完成") + + # Step 3: Overwrite portfolio.json with correct aggregate numbers + # (regenerate_all writes its own format, we fix it back) + print("\n→ 修正 portfolio.json 汇总数据...") + portfolio = { + 'holdings': holdings, + 'cash': cash, + 'total_market_value': market_value, + 'total_assets': round(total_assets, 2), + 'total_pl': 0, + 'position_pct': position_pct, + 'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M'), + 'source': STOCKS_FILE, + } + # DB 写入 + try: + conn = get_conn() + write_holdings_batch(conn, portfolio.get('holdings', [])) + write_portfolio_summary(conn, portfolio) + conn.close() + except Exception as e: + print(f" [DB写入失败] {e}") + + # Step 4: Rebuild decision trees + print("\n→ 重建决策树...") + sys.path.insert(0, '/home/hmo/web-dashboard') + from strategy_tree import init_default_branches + data = read_decisions() + ok = 0 + for e in data.get('decisions', []): + branches = init_default_branches( + e.get('code', ''), e.get('name', ''), + e.get('entry_low', 0), e.get('entry_high', 0), + e.get('stop_loss', 0), e.get('take_profit', 0)) + e['strategy_tree'] = {'branches': branches, 'created_at': datetime.now().strftime('%Y-%m-%d')} + ok += 1 + # DB 写入(替代 json.dump) + try: + conn = get_conn() + for d in data.get('decisions', []): + _whs(conn, d.get('code', ''), d.get('name', ''), d) + conn.close() + except Exception: + pass + # [migrated to DB] — cold backup removed + # with open('/home/hmo/web-dashboard/data/decisions.json', 'w') as f: + # json.dump(data, f, indent=2, ensure_ascii=False) + + print(f"\n{'='*50}") + print(f"导入完成:{len(holdings)}只持仓") + print(f"总资产: {round(total_assets):,.0f}元") + print(f"市值: {market_value:,.0f}元") + print(f"现金: {cash:,.0f}元") + print(f"仓位: {position_pct}%") + print(f"决策树: {ok}/{len(data.get('decisions',[]))}") + + +if __name__ == '__main__': + main() diff --git a/deploy/profile-scripts/premarket_full_review.py b/deploy/profile-scripts/premarket_full_review.py index 8ba4deaf..c27147db 100644 --- a/deploy/profile-scripts/premarket_full_review.py +++ b/deploy/profile-scripts/premarket_full_review.py @@ -12,6 +12,17 @@ import sys, os, json sys.path.insert(0, '/home/hmo/MoFin') +# Step 0: 持仓对账兜底(每日重评前确认 holdings 与 holding_strategies 对齐) +print("=" * 50) +print("🔄 Step 0: 持仓对账兜底") +print("=" * 50) +try: + from holdings_reconciliation import main as _recon + _recon() + print("✅ 持仓对账完成") +except Exception as e: + print(f"⚠️ 持仓对账失败: {e}(继续重评)") + # Step 1: 全量技术参数重评 print("=" * 50) print("📊 盘前全量重评开始")