diff --git a/scripts/holdings_reconciliation.py b/scripts/holdings_reconciliation.py index c04b27f..286ce8a 100644 --- a/scripts/holdings_reconciliation.py +++ b/scripts/holdings_reconciliation.py @@ -1,130 +1,141 @@ -#!/usr/bin/env python3 -"""holdings_reconciliation.py — 每日持仓数据一致性校验 - -在 decisions.json 和 portfolio.json 之间做双向核对: -1. 股数不一致 → 以 portfolio.json 为准(券商导入为源头真理) -2. 股票存在一个文件但不存在另一个 → 同步到双方一致 -3. 总资产重新计算并写入双方 - -24小时内禁止修改策略参数(止盈/止损/买入区),只修股数和总资产。 -""" -import json, sys -from datetime import datetime - -DECISIONS = "/home/hmo/web-dashboard/data/decisions.json" -PORTFOLIO = "/home/hmo/web-dashboard/data/portfolio.json" - -def main(): - dec = json.load(open(DECISIONS)) - pf = json.load(open(PORTFOLIO)) - - # Build maps - dmap = {d["code"]: d for d in dec.get("decisions", [])} - pmap = {h["code"]: h for h in pf.get("holdings", []) if h.get("shares", 0) > 0} - - changes = [] - - # 1. Remove from decisions if not in portfolio (ghost holdings) - for code in list(dmap.keys()): - d = dmap[code] - in_portfolio = code in pmap - if not in_portfolio: - if d.get("shares", 0) > 0: - old = d["shares"] - d["shares"] = 0 - d["type"] = "自选策略" - d.setdefault("changelog", []).append({ - "time": datetime.now().strftime("%Y-%m-%d %H:%M"), - "from": old, - "to": 0, - "reason": "reconciliation: 不在券商持仓" - }) - changes.append(f" {d.get('name','')}({code}): 清仓{old}→0股(不在portfolio)") - continue - # Same stock in both: sync share count (portfolio is source of truth) - p_shares = pmap[code]["shares"] - if d.get("shares", 0) != p_shares: - old = d.get("shares", 0) - d["shares"] = p_shares - d["type"] = "持仓策略" - d.setdefault("changelog", []).append({ - "time": datetime.now().strftime("%Y-%m-%d %H:%M"), - "from": old, - "to": p_shares, - "reason": "reconciliation: 股数与券商一致" - }) - changes.append(f" {d.get('name','')}({code}): 股数{old}→{p_shares}(对齐portfolio)") - - # 2. Add to decisions if in portfolio but not in decisions - for code in pmap: - h = pmap[code] - if code not in dmap: - # Stock is in portfolio but not in decisions → add stub - stub = { - "code": code, - "name": h.get("name", f"STOCK_{code}"), - "shares": h["shares"], - "price": h.get("price", 0), - "stop_loss": 0, - "take_profit": 0, - "entry_low": 0, - "entry_high": 0, - "cost": h.get("cost", 0), - "type": "持仓策略", - "status": "active", - "timing_signal": "持有", - "action": "持仓策略 | 等待技术分析完善", - "tech_snapshot": "", - "action_note": "reconciliation: 自动补充", - "reassessed_at": datetime.now().strftime("%Y-%m-%d %H:%M"), - "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), - "changelog": [{ - "time": datetime.now().strftime("%Y-%m-%d %H:%M"), - "reason": "reconciliation: 券商持仓→自动补充策略" - }], - "trigger": {}, - "analysis": {}, - "currency": "CNY" - } - dec["decisions"].append(stub) - changes.append(f" {stub['name']}({code}): decisions新增持仓({h['shares']}股,来自portfolio)") - - # 3. Recalculate total_assets in portfolio (use mo_models for unified formula) - import sys, os - sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - from mo_models import calc_total_assets - total_assets = calc_total_assets(pf) - dec_total = 0 - for d in dec.get("decisions", []): - if d.get("shares", 0) > 0 and d.get("price", 0) > 0: - dec_total += d["shares"] * d["price"] - - old_total = pf.get("total_assets", 0) - pf["total_assets"] = total_assets - pf["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M") - - # 4. Report - now = datetime.now().strftime("%Y-%m-%d %H:%M") - print(f"【持仓一致性校验】{now}") - print(f"") - if changes: - print(f"修正项 ({len(changes)}):") - for c in changes: - print(c) - else: - print("无差异,全部一致 ✅") - print(f"") - print(f"portfolio stock_value: {stock_value:.2f}") - print(f"portfolio cash: {cash:.2f}") - print(f"portfolio total_assets: {old_total} → {total_assets}") - print(f"decisions stock_value: {dec_total:.2f}") - print(f"decisions count(shares>0): {len([d for d in dec['decisions'] if d.get('shares',0)>0])}") - - # Write - dec["total"] = len(dec["decisions"]) - json.dump(dec, open(DECISIONS, "w"), ensure_ascii=False, indent=2) - json.dump(pf, open(PORTFOLIO, "w"), ensure_ascii=False, indent=2) - print(f"done") - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +"""holdings_reconciliation.py — 每日持仓数据一致性校验 + +在 decisions.json 和 portfolio.json 之间做双向核对: +1. 股数不一致 → 以 portfolio.json 为准(券商导入为源头真理) +2. 股票存在一个文件但不存在另一个 → 同步到双方一致 +3. 总资产重新计算并写入双方 + +24小时内禁止修改策略参数(止盈/止损/买入区),只修股数和总资产。 +""" +import json, sys +from datetime import datetime + +DECISIONS = "/home/hmo/web-dashboard/data/decisions.json" +PORTFOLIO = "/home/hmo/web-dashboard/data/portfolio.json" + +def main(): + dec = json.load(open(DECISIONS)) + pf = json.load(open(PORTFOLIO)) + + # Build maps + dmap = {d["code"]: d for d in dec.get("decisions", [])} + pmap = {h["code"]: h for h in pf.get("holdings", []) if h.get("shares", 0) > 0} + + changes = [] + + # 1. Remove from decisions if not in portfolio (ghost holdings) + for code in list(dmap.keys()): + d = dmap[code] + in_portfolio = code in pmap + if not in_portfolio: + if d.get("shares", 0) > 0: + old = d["shares"] + d["shares"] = 0 + d["type"] = "自选策略" + d.setdefault("changelog", []).append({ + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), + "from": old, + "to": 0, + "reason": "reconciliation: 不在券商持仓" + }) + changes.append(f" {d.get('name','')}({code}): 清仓{old}→0股(不在portfolio)") + continue + # Same stock in both: sync share count (portfolio is source of truth) + p_shares = pmap[code]["shares"] + if d.get("shares", 0) != p_shares: + old = d.get("shares", 0) + d["shares"] = p_shares + d["type"] = "持仓策略" + d.setdefault("changelog", []).append({ + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), + "from": old, + "to": p_shares, + "reason": "reconciliation: 股数与券商一致" + }) + changes.append(f" {d.get('name','')}({code}): 股数{old}→{p_shares}(对齐portfolio)") + + # 2. Add to decisions if in portfolio but not in decisions + for code in pmap: + h = pmap[code] + if code not in dmap: + # Stock is in portfolio but not in decisions → add stub + stub = { + "code": code, + "name": h.get("name", f"STOCK_{code}"), + "shares": h["shares"], + "price": h.get("price", 0), + "stop_loss": 0, + "take_profit": 0, + "entry_low": 0, + "entry_high": 0, + "cost": h.get("cost", 0), + "type": "持仓策略", + "status": "active", + "timing_signal": "持有", + "action": "持仓策略 | 等待技术分析完善", + "tech_snapshot": "", + "action_note": "reconciliation: 自动补充", + "reassessed_at": datetime.now().strftime("%Y-%m-%d %H:%M"), + "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), + "changelog": [{ + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), + "reason": "reconciliation: 券商持仓→自动补充策略" + }], + "trigger": {}, + "analysis": {}, + "currency": "CNY" + } + dec["decisions"].append(stub) + changes.append(f" {stub['name']}({code}): decisions新增持仓({h['shares']}股,来自portfolio)") + + # 3. Recalculate total_assets in portfolio (use mo_models for unified formula) + import sys, os + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + from mo_models import calc_total_assets + total_assets = calc_total_assets(pf) + dec_total = 0 + for d in dec.get("decisions", []): + if d.get("shares", 0) > 0 and d.get("price", 0) > 0: + dec_total += d["shares"] * d["price"] + + old_total = pf.get("total_assets", 0) + pf["total_assets"] = total_assets + pf["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M") + + # 4. Report + now = datetime.now().strftime("%Y-%m-%d %H:%M") + print(f"【持仓一致性校验】{now}") + print(f"") + if changes: + print(f"修正项 ({len(changes)}):") + for c in changes: + print(c) + else: + print("无差异,全部一致 ✅") + print(f"") + print(f"portfolio stock_value: {stock_value:.2f}") + print(f"portfolio cash: {cash:.2f}") + print(f"portfolio total_assets: {old_total} → {total_assets}") + print(f"decisions stock_value: {dec_total:.2f}") + print(f"decisions count(shares>0): {len([d for d in dec['decisions'] if d.get('shares',0)>0])}") + + # Write — DB 优先(强制币种约束),JSON 冷备 + dec["total"] = len(dec["decisions"]) + try: + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy + conn = get_conn() + write_holdings_batch(conn, pf.get('holdings', [])) + write_portfolio_summary(conn, pf) + for d in dec.get('decisions', []): + write_holding_strategy(conn, d.get('code', ''), d.get('name', ''), d) + conn.close() + except Exception as e: + print(f" [DB写入失败] {e}") + json.dump(dec, open(DECISIONS, "w"), ensure_ascii=False, indent=2) + json.dump(pf, open(PORTFOLIO, "w"), ensure_ascii=False, indent=2) + print(f"done") + +if __name__ == "__main__": + main() diff --git a/scripts/import_holding_xls.py b/scripts/import_holding_xls.py index 0865474..8e16981 100755 --- a/scripts/import_holding_xls.py +++ b/scripts/import_holding_xls.py @@ -1,173 +1,182 @@ -#!/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 = float(clean_cell(r[5])) - pl = float(clean_cell(r[6])) if r[6].strip() else 0 - mkt_val = float(clean_cell(r[11])) - cost_amount = 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 - mv_cny = mkt_val - total_mv_cny += mv_cny - - holdings.append({ - 'code': code, 'name': name, 'shares': shares, - 'price': price, 'cost_price': round(cost_price, 2), - 'currency': currency, 'market_val': mkt_val, - 'cost_amount': cost_amount, '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, position_pct, added_at, is_active) - VALUES (?, ?, ?, ?, ?, ?, 1) - ''', (h['code'], h['name'], h['shares'], h['cost_price'], - 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) - - # 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() +#!/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 = float(clean_cell(r[5])) + pl = float(clean_cell(r[6])) if r[6].strip() else 0 + mkt_val = float(clean_cell(r[11])) + cost_amount = 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 + mv_cny = mkt_val + total_mv_cny += mv_cny + + holdings.append({ + 'code': code, 'name': name, 'shares': shares, + 'price': price, 'cost_price': round(cost_price, 2), + 'currency': currency, 'market_val': mkt_val, + 'cost_amount': cost_amount, '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, position_pct, added_at, is_active) + VALUES (?, ?, ?, ?, ?, ?, 1) + ''', (h['code'], h['name'], h['shares'], h['cost_price'], + 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() diff --git a/scripts/process_trade.py b/scripts/process_trade.py index 84d41df..e955435 100644 --- a/scripts/process_trade.py +++ b/scripts/process_trade.py @@ -1,137 +1,148 @@ -#!/usr/bin/env python3 -"""process_trade.py — 处理交易截图,更新 portfolio.json - -Dad 发交易截图后,填入以下信息运行此脚本: - python3 process_trade.py --action buy --code 600563 --shares 100 --price 189.20 - -它会: -1. 更新 holdings(加/减股数,归零则移除) -2. 更新 cash(买入减现金,卖出加现金) -3. 同步更新 decisions.json 的 shares 字段 -4. 记录 changelog - -用法: - python3 process_trade.py --action sell --code 600563 --shares 100 --price 189.20 - python3 process_trade.py --action buy --code 300308 --shares 50 --price 1230.00 -""" -import json, sys, os -from datetime import datetime - -PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json" -DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json" - -def parse_args(): - args = {} - for i, a in enumerate(sys.argv[1:]): - if a.startswith("--"): - key = a.lstrip("-") - val = sys.argv[i+2] if i+2 < len(sys.argv) and not sys.argv[i+2].startswith("--") else None - args[key] = val - return args - -def main(): - args = parse_args() - action = args.get("action", "") - code = args.get("code", "") - shares = int(float(args.get("shares", 0))) - price = float(args.get("price", 0)) - name = args.get("name", "") - - if not action or not code or not shares or not price: - print("用法: python3 process_trade.py --action sell --code 600563 --shares 100 --price 189.20") - sys.exit(1) - - now = datetime.now().strftime("%Y-%m-%d %H:%M") - cost = shares * price - - # 读数据 - pf = json.load(open(PORTFOLIO_PATH)) - dec = json.load(open(DECISIONS_PATH)) - - if action == "sell": - # 找持仓 - found = None - for h in pf["holdings"]: - if h["code"] == code: - found = h - break - if not found: - print(f"❌ 错误: 代码 {code} 未在持仓中找到") - sys.exit(1) - old_shares = found.get("shares", 0) - if old_shares < shares: - print(f"❌ 错误: 持仓只有 {old_shares} 股,不够卖 {shares} 股") - sys.exit(1) - # 减股数 - found["shares"] = old_shares - shares - found["updated_at"] = now - # 归零则移除 - if found["shares"] <= 0: - pf["holdings"] = [h for h in pf["holdings"] if h["code"] != code] - print(f" 已全部清仓,从持仓移除") - # 加现金 - old_cash = pf.get("cash", 0) or 0 - pf["cash"] = round(old_cash + cost, 2) - print(f" ✅ 卖出 {name}({code}) {shares}股 @{price} = {cost:.2f}") - print(f" 现金: {old_cash} → {pf['cash']}") - - elif action == "buy": - # 找是否已有该股 - found = None - for h in pf["holdings"]: - if h["code"] == code: - found = h - break - if found: - # 加权平均成本 - old_shares = found.get("shares", 0) or 0 - old_cost = found.get("cost", 0) or 0 - total_cost = old_cost * old_shares + cost - new_shares = old_shares + shares - new_avg_cost = round(total_cost / new_shares, 2) if new_shares > 0 else price - found["shares"] = new_shares - found["cost"] = new_avg_cost - found["updated_at"] = now - else: - pf["holdings"].append({ - "code": code, "name": name, "shares": shares, - "cost": price, "price": price, "updated_at": now - }) - # 减现金 - old_cash = pf.get("cash", 0) or 0 - pf["cash"] = round(old_cash - cost, 2) - print(f" ✅ 买入 {name}({code}) {shares}股 @{price} = {cost:.2f}") - print(f" 现金: {old_cash} → {pf['cash']}") - - # 同步 decisions.json 的 shares - for d in dec.get("decisions", []): - if d["code"] == code: - old_dec_shares = d.get("shares", 0) or 0 - d["shares"] = (d.get("shares", 0) or 0) + (shares if action == "buy" else -shares) - if d["shares"] <= 0 and action == "sell": - d["shares"] = 0 - d["type"] = "自选策略" - d.setdefault("changelog", []).append({ - "time": now, - "event": action, - "shares": shares, - "price": price, - "total": cost - }) - break - - # 写入 - pf["updated_at"] = now - json.dump(pf, open(PORTFOLIO_PATH, "w"), indent=2, ensure_ascii=False) - json.dump(dec, open(DECISIONS_PATH, "w"), indent=2, ensure_ascii=False) - - # 重算总资产 - total_mv = sum((h.get("shares",0) or 0) * (h.get("price",0) or 0) for h in pf["holdings"]) - total = round(total_mv + (pf.get("cash",0) or 0), 2) - print(f"\n📊 持仓市值: {total_mv:.2f}") - print(f"📊 现金: {pf.get('cash',0):.2f}") - print(f"📊 总资产: {total:.2f}") - print(f"📊 持仓 {len(pf['holdings'])} 只") - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +"""process_trade.py — 处理交易截图,更新 portfolio.json + +Dad 发交易截图后,填入以下信息运行此脚本: + python3 process_trade.py --action buy --code 600563 --shares 100 --price 189.20 + +它会: +1. 更新 holdings(加/减股数,归零则移除) +2. 更新 cash(买入减现金,卖出加现金) +3. 同步更新 decisions.json 的 shares 字段 +4. 记录 changelog + +用法: + python3 process_trade.py --action sell --code 600563 --shares 100 --price 189.20 + python3 process_trade.py --action buy --code 300308 --shares 50 --price 1230.00 +""" +import json, sys, os +from datetime import datetime + +PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json" +DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json" + +def parse_args(): + args = {} + for i, a in enumerate(sys.argv[1:]): + if a.startswith("--"): + key = a.lstrip("-") + val = sys.argv[i+2] if i+2 < len(sys.argv) and not sys.argv[i+2].startswith("--") else None + args[key] = val + return args + +def main(): + args = parse_args() + action = args.get("action", "") + code = args.get("code", "") + shares = int(float(args.get("shares", 0))) + price = float(args.get("price", 0)) + name = args.get("name", "") + + if not action or not code or not shares or not price: + print("用法: python3 process_trade.py --action sell --code 600563 --shares 100 --price 189.20") + sys.exit(1) + + now = datetime.now().strftime("%Y-%m-%d %H:%M") + cost = shares * price + + # 读数据 + pf = json.load(open(PORTFOLIO_PATH)) + dec = json.load(open(DECISIONS_PATH)) + + if action == "sell": + # 找持仓 + found = None + for h in pf["holdings"]: + if h["code"] == code: + found = h + break + if not found: + print(f"❌ 错误: 代码 {code} 未在持仓中找到") + sys.exit(1) + old_shares = found.get("shares", 0) + if old_shares < shares: + print(f"❌ 错误: 持仓只有 {old_shares} 股,不够卖 {shares} 股") + sys.exit(1) + # 减股数 + found["shares"] = old_shares - shares + found["updated_at"] = now + # 归零则移除 + if found["shares"] <= 0: + pf["holdings"] = [h for h in pf["holdings"] if h["code"] != code] + print(f" 已全部清仓,从持仓移除") + # 加现金 + old_cash = pf.get("cash", 0) or 0 + pf["cash"] = round(old_cash + cost, 2) + print(f" ✅ 卖出 {name}({code}) {shares}股 @{price} = {cost:.2f}") + print(f" 现金: {old_cash} → {pf['cash']}") + + elif action == "buy": + # 找是否已有该股 + found = None + for h in pf["holdings"]: + if h["code"] == code: + found = h + break + if found: + # 加权平均成本 + old_shares = found.get("shares", 0) or 0 + old_cost = found.get("cost", 0) or 0 + total_cost = old_cost * old_shares + cost + new_shares = old_shares + shares + new_avg_cost = round(total_cost / new_shares, 2) if new_shares > 0 else price + found["shares"] = new_shares + found["cost"] = new_avg_cost + found["updated_at"] = now + else: + pf["holdings"].append({ + "code": code, "name": name, "shares": shares, + "cost": price, "price": price, "updated_at": now + }) + # 减现金 + old_cash = pf.get("cash", 0) or 0 + pf["cash"] = round(old_cash - cost, 2) + print(f" ✅ 买入 {name}({code}) {shares}股 @{price} = {cost:.2f}") + print(f" 现金: {old_cash} → {pf['cash']}") + + # 同步 decisions.json 的 shares + for d in dec.get("decisions", []): + if d["code"] == code: + old_dec_shares = d.get("shares", 0) or 0 + d["shares"] = (d.get("shares", 0) or 0) + (shares if action == "buy" else -shares) + if d["shares"] <= 0 and action == "sell": + d["shares"] = 0 + d["type"] = "自选策略" + d.setdefault("changelog", []).append({ + "time": now, + "event": action, + "shares": shares, + "price": price, + "total": cost + }) + break + + # 写入 — DB 优先 + pf["updated_at"] = now + try: + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy + conn = get_conn() + write_holdings_batch(conn, pf.get('holdings', [])) + write_portfolio_summary(conn, pf) + for d in dec.get('decisions', []): + write_holding_strategy(conn, d.get('code', ''), d.get('name', ''), d) + conn.close() + except Exception: + pass + json.dump(pf, open(PORTFOLIO_PATH, "w"), indent=2, ensure_ascii=False) + json.dump(dec, open(DECISIONS_PATH, "w"), indent=2, ensure_ascii=False) + + # 重算总资产 + total_mv = sum((h.get("shares",0) or 0) * (h.get("price",0) or 0) for h in pf["holdings"]) + total = round(total_mv + (pf.get("cash",0) or 0), 2) + print(f"\n📊 持仓市值: {total_mv:.2f}") + print(f"📊 现金: {pf.get('cash',0):.2f}") + print(f"📊 总资产: {total:.2f}") + print(f"📊 持仓 {len(pf['holdings'])} 只") + +if __name__ == "__main__": + main()