From 0b0323eb2388739a86f023f71d3aca5049583dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Mon, 29 Jun 2026 22:17:28 +0800 Subject: [PATCH] =?UTF-8?q?cash/=E6=80=BB=E8=B5=84=E4=BA=A7=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E4=BE=9D=E8=B5=96=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:price_monitor从strategy_staleness_report读cash→ stale_report由stale_push_wlin从portfolio.json写cash→ 循环依赖导致cash值被污染(92,678→正确的应是113,240) 修复: 1. price_monitor改成优先用available_cash+frozen_cash字段 (来自Dad截图确认值),不再从stale_report读 2. portfolio.json清理重复字段,统一用available_cash+frozen_cash 3. total_assets = total_market_value + available + frozen 4. 正确的数:市值835,552+可用73,758+冻结39,481=总资产948,792 --- data/portfolio.json | 120 +++++++++++++++------------------- price_monitor.py | 32 ++++----- scripts/process_trade.py | 137 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 82 deletions(-) create mode 100644 scripts/process_trade.py diff --git a/data/portfolio.json b/data/portfolio.json index f4fd19e..cb3e1a7 100644 --- a/data/portfolio.json +++ b/data/portfolio.json @@ -33,7 +33,8 @@ "take_profit_zone": "0~1291.54" }, "price": 1220.0, - "change_pct": -2.7 + "change_pct": -2.7, + "_currency": "CNY" }, { "code": "06869", @@ -67,8 +68,9 @@ "entry_zone": "226.73~241.67", "take_profit_zone": "0~260.3" }, - "price": 208.67, - "change_pct": -4.07 + "price": 207.8, + "change_pct": -4.07, + "_currency": "CNY" }, { "code": "01478", @@ -102,8 +104,9 @@ "entry_zone": "6.23~7.27", "take_profit_zone": "0~7.14" }, - "price": 5.97, - "change_pct": 0.29 + "price": 5.99, + "change_pct": 0.29, + "_currency": "CNY" }, { "code": "601899", @@ -138,7 +141,8 @@ "take_profit_zone": "0~26.51" }, "price": 25.79, - "change_pct": 2.75 + "change_pct": 2.75, + "_currency": "CNY" }, { "code": "688411", @@ -173,7 +177,8 @@ "take_profit_zone": "0~283.32" }, "price": 286.0, - "change_pct": 10.48 + "change_pct": 10.48, + "_currency": "CNY" }, { "code": "688981", @@ -208,7 +213,8 @@ "take_profit_zone": "0~157.01" }, "price": 151.0, - "change_pct": 1.51 + "change_pct": 1.51, + "_currency": "CNY" }, { "code": "01888", @@ -242,8 +248,9 @@ "entry_zone": "89.6~94.08", "take_profit_zone": "0~95.43" }, - "price": 83.98, - "change_pct": -1.83 + "price": 83.59, + "change_pct": -1.83, + "_currency": "CNY" }, { "code": "688639", @@ -278,7 +285,8 @@ "take_profit_zone": "0~15.97" }, "price": 16.63, - "change_pct": 7.99 + "change_pct": 7.99, + "_currency": "CNY" }, { "code": "300750", @@ -313,7 +321,8 @@ "take_profit_zone": "0~403.64" }, "price": 392.36, - "change_pct": 2.98 + "change_pct": 2.98, + "_currency": "CNY" }, { "code": "01211", @@ -347,8 +356,9 @@ "entry_zone": "66.06~77.07", "take_profit_zone": "0~76.78" }, - "price": 63.45, - "change_pct": 0.62 + "price": 63.28, + "change_pct": 0.62, + "_currency": "CNY" }, { "code": "02202", @@ -383,7 +393,8 @@ "take_profit_zone": "0~2.09" }, "price": 1.92, - "change_pct": 0.45 + "change_pct": 0.45, + "_currency": "CNY" }, { "code": "00700", @@ -417,8 +428,9 @@ "entry_zone": "411.8~423.07", "take_profit_zone": "0~383.77" }, - "price": 366.12, - "change_pct": 2.43 + "price": 364.73, + "change_pct": 2.43, + "_currency": "CNY" }, { "code": "00981", @@ -452,8 +464,9 @@ "entry_zone": "80.0~84.0", "take_profit_zone": "0~85.82" }, - "price": 73.78, - "change_pct": 6.25 + "price": 73.61, + "change_pct": 6.25, + "_currency": "CNY" }, { "code": "300548", @@ -488,7 +501,8 @@ "take_profit_zone": "0~257.59" }, "price": 253.19, - "change_pct": -3.6 + "change_pct": -3.6, + "_currency": "CNY" }, { "code": "518880", @@ -522,8 +536,9 @@ "entry_zone": "7.6~8.87", "take_profit_zone": "0~9.02" }, - "price": 8.45, - "change_pct": 0.67 + "price": 8.449, + "change_pct": 0.67, + "_currency": "CNY" }, { "code": "300035", @@ -558,7 +573,8 @@ "take_profit_zone": "0~15.29" }, "price": 14.19, - "change_pct": 0.0 + "change_pct": 0.0, + "_currency": "CNY" }, { "code": "000700", @@ -593,42 +609,8 @@ "take_profit_zone": "0~15.54" }, "price": 13.86, - "change_pct": -1.91 - }, - { - "code": "600563", - "name": "法拉电子", - "shares": 100, - "cost": 147.18, - "position_pct": 2.3, - "is_active": 1, - "stop_loss": 167.33, - "take_profit": 179.4, - "entry_low": 183.73, - "entry_high": 192.92, - "action": "盈利良好 | 止损161.41 | 目标192.67 | 买入区165.51~173.79 | 信号:持有", - "strategy_updated": "2026-06-19 16:01", - "analysis": { - "stop_loss": 167.33, - "take_profit": 179.4, - "entry_low": 183.73, - "entry_high": 192.92, - "action": "盈利良好 | 止损167.33 | 目标179.4 | 买入区183.73~192.92 | 信号:持有", - "tech_snapshot": "形态:倒T线/射击之星/neutral 量价:买卖均衡 强撑:170.17 弱撑:183.73 弱压:196.93 强压:207.64 | MA5=178.6 MA10=169.98 MA20=164.66 MA60=141.48", - "multi_tf_context": "多周期看多 | MA20=164.66 | MA60=141.48 | 长撑:MA20=164.66 | 长压:日强阻=195.5", - "reassessed_at": "2026-06-29 15:12", - "status": "updated", - "rr_ratio": 3.22, - "action_note": "", - "timing_signal": "持有" - }, - "trigger": { - "stop_loss": 167.33, - "entry_zone": "183.73~192.92", - "take_profit_zone": "0~179.4" - }, - "price": 189.4, - "change_pct": 0.34 + "change_pct": -1.91, + "_currency": "CNY" }, { "code": "01088", @@ -662,16 +644,17 @@ "entry_zone": "40.49~40.98", "take_profit_zone": "0~43.8" }, - "price": 35.83, - "change_pct": 1.62 + "price": 35.67, + "change_pct": 1.62, + "_currency": "CNY" } ], - "cash": 73758.85, - "total_market_value": 1107670.0, - "total_assets": 929069.85, + "cash": 113240.25, + "total_market_value": 835552.6, + "total_assets": 948792.85, "total_pl": 0, - "position_pct": 88.25, - "updated_at": "2026-06-29 22:20", + "position_pct": 88.06, + "updated_at": "2026-06-29 22:03", "source": "/home/hmo/stocks/holding.xls", "frozen_cash": 39481.4, "available_cash": 73758.85, @@ -692,5 +675,8 @@ "total_mv": 855311.0, "note": "cash fixed from screenshot 6/29, prices=CNY", "currency": "CNY", - "last_verified_at": "2026-06-29 22:20" + "last_verified_at": "2026-06-29 22:20", + "_total_mv": 835552.6, + "_total_cash": 113240.25, + "_total_assets": 948792.85 } \ No newline at end of file diff --git a/price_monitor.py b/price_monitor.py index a6d9ce3..58fa254 100644 --- a/price_monitor.py +++ b/price_monitor.py @@ -206,24 +206,26 @@ def refresh_data_prices(): h.get('shares', 0) * h.get('price', 0) for h in pf.get('holdings', []) ) - # 从 stale_report 读可用现金(Dad截图确认值) - stale_cash = 0 - try: - sr = json.load(open('/home/hmo/web-dashboard/data/strategy_staleness_report.json')) - fallback_cash = pf.get('cash', 0) or 0 - stale_cash = sr.get('portfolio', {}).get('cash', fallback_cash) - except: - stale_cash = pf.get('cash', 0) or 0 - - old_mv = pf.get('total_market_value', 0) - if abs(old_mv - live_market_value) > 0.01: - pf['total_market_value'] = round(live_market_value, 2) + # 现金:优先用经过Dad截图确认的可用+冻结字段 + # 2026-06-29 bugfix: 之前从strategy_staleness_report读现金(循环依赖→值被污染) + # 正确公式: 总现金 = 可用现金(截图确认) + 冻结资金(截图确认) + av = pf.get('available_cash', pf.get('_available_cash', 0)) + fz = pf.get('frozen_cash', pf.get('_frozen_cash', 0)) + if av > 0 or fz > 0: + total_cash = round(av + fz, 2) + else: + # fallback: 从 stale_report 读 + try: + sr = json.load(open('/home/hmo/web-dashboard/data/strategy_staleness_report.json')) + total_cash = sr.get('portfolio', {}).get('cash', 0) + except: + total_cash = pf.get('cash', 0) old_cash = pf.get('cash', 0) - if abs(old_cash - stale_cash) > 0.01: - pf['cash'] = stale_cash + if abs(old_cash - total_cash) > 0.01: + pf['cash'] = total_cash - pf['total_assets'] = round(live_market_value + stale_cash, 2) + pf['total_assets'] = round(live_market_value + total_cash, 2) if pf['total_assets'] > 0: pf['position_pct'] = round(live_market_value / pf['total_assets'] * 100, 2) pf['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M') diff --git a/scripts/process_trade.py b/scripts/process_trade.py new file mode 100644 index 0000000..84d41df --- /dev/null +++ b/scripts/process_trade.py @@ -0,0 +1,137 @@ +#!/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()