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:
+53
-67
@@ -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
|
||||
}
|
||||
+14
-12
@@ -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
|
||||
# 现金:优先用经过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'))
|
||||
fallback_cash = pf.get('cash', 0) or 0
|
||||
stale_cash = sr.get('portfolio', {}).get('cash', fallback_cash)
|
||||
total_cash = sr.get('portfolio', {}).get('cash', 0)
|
||||
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)
|
||||
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')
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user