feat: holdings_reconciliation + process_trade + import_holding_xls → DB writes
This commit is contained in:
+148
-137
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user