Files
MoFin/scripts/import_holding_xls.py
T

192 lines
7.4 KiB
Python
Executable File
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
"""
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
STOCKS_FILE = "/home/hmo/stocks/holding.xls"
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
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
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])
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
# 港股:所有金额必须转为 CNY 再存储(mo_models 设计规范:portfolio 应全部存 CNY
if currency == 'HKD':
cost_price = round(cost_price_raw * rate, 2)
mv_cny = round(mkt_val_raw * rate, 2)
price_cny = round(price * rate, 2)
else:
cost_price = round(cost_price_raw, 2)
mv_cny = mkt_val_raw
price_cny = price
total_mv_cny += mv_cny
holdings.append({
'code': code, 'name': name, 'shares': shares,
'price': price_cny, 'cost_price': cost_price,
'currency': 'CNY', '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:
frozen_cash = float(args.get('frozen', pf.get('frozen_cash', 0)) or 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'], 'CNY',
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,
}
with open(PORTFOLIO_PATH, 'w') as f:
json.dump(portfolio, f, indent=2, ensure_ascii=False)
# DB 写入
try:
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary
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
with open('/home/hmo/web-dashboard/data/decisions.json') as f:
data = json.load(f)
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
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()