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