refactor(C组): clean_watchlist改为函数+import_holding_xls增加调用+premarket_full_review增加兜底对账

This commit is contained in:
xxm
2026-08-20 22:54:46 +08:00
parent 8c880e1b7c
commit 99cce27a2c
3 changed files with 344 additions and 326 deletions
+132 -125
View File
@@ -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()
+201 -201
View File
@@ -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()
@@ -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("📊 盘前全量重评开始")