cash/总资产循环依赖修复

问题: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
This commit is contained in:
知微
2026-06-29 22:17:28 +08:00
parent 9709c43ccb
commit 0b0323eb23
3 changed files with 207 additions and 82 deletions
+53 -67
View File
@@ -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
}
+17 -15
View File
@@ -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')
+137
View File
@@ -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()