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
|
#!/usr/bin/env python3
|
||||||
"""holdings_reconciliation.py — 每日持仓数据一致性校验
|
"""holdings_reconciliation.py — 每日持仓数据一致性校验
|
||||||
|
|
||||||
在 decisions.json 和 portfolio.json 之间做双向核对:
|
在 decisions.json 和 portfolio.json 之间做双向核对:
|
||||||
1. 股数不一致 → 以 portfolio.json 为准(券商导入为源头真理)
|
1. 股数不一致 → 以 portfolio.json 为准(券商导入为源头真理)
|
||||||
2. 股票存在一个文件但不存在另一个 → 同步到双方一致
|
2. 股票存在一个文件但不存在另一个 → 同步到双方一致
|
||||||
3. 总资产重新计算并写入双方
|
3. 总资产重新计算并写入双方
|
||||||
|
|
||||||
24小时内禁止修改策略参数(止盈/止损/买入区),只修股数和总资产。
|
24小时内禁止修改策略参数(止盈/止损/买入区),只修股数和总资产。
|
||||||
"""
|
"""
|
||||||
import json, sys
|
import json, sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
DECISIONS = "/home/hmo/web-dashboard/data/decisions.json"
|
DECISIONS = "/home/hmo/web-dashboard/data/decisions.json"
|
||||||
PORTFOLIO = "/home/hmo/web-dashboard/data/portfolio.json"
|
PORTFOLIO = "/home/hmo/web-dashboard/data/portfolio.json"
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
dec = json.load(open(DECISIONS))
|
dec = json.load(open(DECISIONS))
|
||||||
pf = json.load(open(PORTFOLIO))
|
pf = json.load(open(PORTFOLIO))
|
||||||
|
|
||||||
# Build maps
|
# Build maps
|
||||||
dmap = {d["code"]: d for d in dec.get("decisions", [])}
|
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}
|
pmap = {h["code"]: h for h in pf.get("holdings", []) if h.get("shares", 0) > 0}
|
||||||
|
|
||||||
changes = []
|
changes = []
|
||||||
|
|
||||||
# 1. Remove from decisions if not in portfolio (ghost holdings)
|
# 1. Remove from decisions if not in portfolio (ghost holdings)
|
||||||
for code in list(dmap.keys()):
|
for code in list(dmap.keys()):
|
||||||
d = dmap[code]
|
d = dmap[code]
|
||||||
in_portfolio = code in pmap
|
in_portfolio = code in pmap
|
||||||
if not in_portfolio:
|
if not in_portfolio:
|
||||||
if d.get("shares", 0) > 0:
|
if d.get("shares", 0) > 0:
|
||||||
old = d["shares"]
|
old = d["shares"]
|
||||||
d["shares"] = 0
|
d["shares"] = 0
|
||||||
d["type"] = "自选策略"
|
d["type"] = "自选策略"
|
||||||
d.setdefault("changelog", []).append({
|
d.setdefault("changelog", []).append({
|
||||||
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
"from": old,
|
"from": old,
|
||||||
"to": 0,
|
"to": 0,
|
||||||
"reason": "reconciliation: 不在券商持仓"
|
"reason": "reconciliation: 不在券商持仓"
|
||||||
})
|
})
|
||||||
changes.append(f" {d.get('name','')}({code}): 清仓{old}→0股(不在portfolio)")
|
changes.append(f" {d.get('name','')}({code}): 清仓{old}→0股(不在portfolio)")
|
||||||
continue
|
continue
|
||||||
# Same stock in both: sync share count (portfolio is source of truth)
|
# Same stock in both: sync share count (portfolio is source of truth)
|
||||||
p_shares = pmap[code]["shares"]
|
p_shares = pmap[code]["shares"]
|
||||||
if d.get("shares", 0) != p_shares:
|
if d.get("shares", 0) != p_shares:
|
||||||
old = d.get("shares", 0)
|
old = d.get("shares", 0)
|
||||||
d["shares"] = p_shares
|
d["shares"] = p_shares
|
||||||
d["type"] = "持仓策略"
|
d["type"] = "持仓策略"
|
||||||
d.setdefault("changelog", []).append({
|
d.setdefault("changelog", []).append({
|
||||||
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
"from": old,
|
"from": old,
|
||||||
"to": p_shares,
|
"to": p_shares,
|
||||||
"reason": "reconciliation: 股数与券商一致"
|
"reason": "reconciliation: 股数与券商一致"
|
||||||
})
|
})
|
||||||
changes.append(f" {d.get('name','')}({code}): 股数{old}→{p_shares}(对齐portfolio)")
|
changes.append(f" {d.get('name','')}({code}): 股数{old}→{p_shares}(对齐portfolio)")
|
||||||
|
|
||||||
# 2. Add to decisions if in portfolio but not in decisions
|
# 2. Add to decisions if in portfolio but not in decisions
|
||||||
for code in pmap:
|
for code in pmap:
|
||||||
h = pmap[code]
|
h = pmap[code]
|
||||||
if code not in dmap:
|
if code not in dmap:
|
||||||
# Stock is in portfolio but not in decisions → add stub
|
# Stock is in portfolio but not in decisions → add stub
|
||||||
stub = {
|
stub = {
|
||||||
"code": code,
|
"code": code,
|
||||||
"name": h.get("name", f"STOCK_{code}"),
|
"name": h.get("name", f"STOCK_{code}"),
|
||||||
"shares": h["shares"],
|
"shares": h["shares"],
|
||||||
"price": h.get("price", 0),
|
"price": h.get("price", 0),
|
||||||
"stop_loss": 0,
|
"stop_loss": 0,
|
||||||
"take_profit": 0,
|
"take_profit": 0,
|
||||||
"entry_low": 0,
|
"entry_low": 0,
|
||||||
"entry_high": 0,
|
"entry_high": 0,
|
||||||
"cost": h.get("cost", 0),
|
"cost": h.get("cost", 0),
|
||||||
"type": "持仓策略",
|
"type": "持仓策略",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"timing_signal": "持有",
|
"timing_signal": "持有",
|
||||||
"action": "持仓策略 | 等待技术分析完善",
|
"action": "持仓策略 | 等待技术分析完善",
|
||||||
"tech_snapshot": "",
|
"tech_snapshot": "",
|
||||||
"action_note": "reconciliation: 自动补充",
|
"action_note": "reconciliation: 自动补充",
|
||||||
"reassessed_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
"reassessed_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
"changelog": [{
|
"changelog": [{
|
||||||
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
"time": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
"reason": "reconciliation: 券商持仓→自动补充策略"
|
"reason": "reconciliation: 券商持仓→自动补充策略"
|
||||||
}],
|
}],
|
||||||
"trigger": {},
|
"trigger": {},
|
||||||
"analysis": {},
|
"analysis": {},
|
||||||
"currency": "CNY"
|
"currency": "CNY"
|
||||||
}
|
}
|
||||||
dec["decisions"].append(stub)
|
dec["decisions"].append(stub)
|
||||||
changes.append(f" {stub['name']}({code}): decisions新增持仓({h['shares']}股,来自portfolio)")
|
changes.append(f" {stub['name']}({code}): decisions新增持仓({h['shares']}股,来自portfolio)")
|
||||||
|
|
||||||
# 3. Recalculate total_assets in portfolio (use mo_models for unified formula)
|
# 3. Recalculate total_assets in portfolio (use mo_models for unified formula)
|
||||||
import sys, os
|
import sys, os
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from mo_models import calc_total_assets
|
from mo_models import calc_total_assets
|
||||||
total_assets = calc_total_assets(pf)
|
total_assets = calc_total_assets(pf)
|
||||||
dec_total = 0
|
dec_total = 0
|
||||||
for d in dec.get("decisions", []):
|
for d in dec.get("decisions", []):
|
||||||
if d.get("shares", 0) > 0 and d.get("price", 0) > 0:
|
if d.get("shares", 0) > 0 and d.get("price", 0) > 0:
|
||||||
dec_total += d["shares"] * d["price"]
|
dec_total += d["shares"] * d["price"]
|
||||||
|
|
||||||
old_total = pf.get("total_assets", 0)
|
old_total = pf.get("total_assets", 0)
|
||||||
pf["total_assets"] = total_assets
|
pf["total_assets"] = total_assets
|
||||||
pf["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
pf["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
# 4. Report
|
# 4. Report
|
||||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
print(f"【持仓一致性校验】{now}")
|
print(f"【持仓一致性校验】{now}")
|
||||||
print(f"")
|
print(f"")
|
||||||
if changes:
|
if changes:
|
||||||
print(f"修正项 ({len(changes)}):")
|
print(f"修正项 ({len(changes)}):")
|
||||||
for c in changes:
|
for c in changes:
|
||||||
print(c)
|
print(c)
|
||||||
else:
|
else:
|
||||||
print("无差异,全部一致 ✅")
|
print("无差异,全部一致 ✅")
|
||||||
print(f"")
|
print(f"")
|
||||||
print(f"portfolio stock_value: {stock_value:.2f}")
|
print(f"portfolio stock_value: {stock_value:.2f}")
|
||||||
print(f"portfolio cash: {cash:.2f}")
|
print(f"portfolio cash: {cash:.2f}")
|
||||||
print(f"portfolio total_assets: {old_total} → {total_assets}")
|
print(f"portfolio total_assets: {old_total} → {total_assets}")
|
||||||
print(f"decisions stock_value: {dec_total:.2f}")
|
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])}")
|
print(f"decisions count(shares>0): {len([d for d in dec['decisions'] if d.get('shares',0)>0])}")
|
||||||
|
|
||||||
# Write
|
# Write — DB 优先(强制币种约束),JSON 冷备
|
||||||
dec["total"] = len(dec["decisions"])
|
dec["total"] = len(dec["decisions"])
|
||||||
json.dump(dec, open(DECISIONS, "w"), ensure_ascii=False, indent=2)
|
try:
|
||||||
json.dump(pf, open(PORTFOLIO, "w"), ensure_ascii=False, indent=2)
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
print(f"done")
|
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy
|
||||||
|
conn = get_conn()
|
||||||
if __name__ == "__main__":
|
write_holdings_batch(conn, pf.get('holdings', []))
|
||||||
main()
|
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()
|
||||||
|
|||||||
+182
-173
@@ -1,173 +1,182 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
import_holding_xls.py — 从 holding.xls 导入持仓到全系统
|
import_holding_xls.py — 从 holding.xls 导入持仓到全系统
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python3 import_holding_xls.py [--cash 现金] [--total 总资产] [--mv 市值]
|
python3 import_holding_xls.py [--cash 现金] [--total 总资产] [--mv 市值]
|
||||||
|
|
||||||
--cash 必传!holding文件不含现金行,不传则现金=0。
|
--cash 必传!holding文件不含现金行,不传则现金=0。
|
||||||
|
|
||||||
不传 --total/--mv 则从 holding.xls 计算(可能有价格时差误差)。
|
不传 --total/--mv 则从 holding.xls 计算(可能有价格时差误差)。
|
||||||
建议传截图上的真实数字。
|
建议传截图上的真实数字。
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
python3 import_holding_xls.py --cash 73758.0 --total 874598.90 --mv 800840.90
|
python3 import_holding_xls.py --cash 73758.0 --total 874598.90 --mv 800840.90
|
||||||
"""
|
"""
|
||||||
import csv, json, sys, subprocess, sqlite3, os
|
import csv, json, sys, subprocess, sqlite3, os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
STOCKS_FILE = "/home/hmo/stocks/holding.xls"
|
STOCKS_FILE = "/home/hmo/stocks/holding.xls"
|
||||||
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
|
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
|
||||||
DB_PATH = "/home/hmo/web-dashboard/data/mofin.db"
|
DB_PATH = "/home/hmo/web-dashboard/data/mofin.db"
|
||||||
|
|
||||||
|
|
||||||
def clean_cell(v):
|
def clean_cell(v):
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if v.startswith('="') and v.endswith('"'):
|
if v.startswith('="') and v.endswith('"'):
|
||||||
v = v[2:-1]
|
v = v[2:-1]
|
||||||
elif v.startswith('='):
|
elif v.startswith('='):
|
||||||
v = v[1:]
|
v = v[1:]
|
||||||
return v.strip()
|
return v.strip()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Parse args
|
# Parse args
|
||||||
# ⚠️ 现金不从holding文件读取。holding只有股票持仓,现金必须单独提供(截图)。
|
# ⚠️ 现金不从holding文件读取。holding只有股票持仓,现金必须单独提供(截图)。
|
||||||
# 不传 --cash 则默认为0,会在后面警告。
|
# 不传 --cash 则默认为0,会在后面警告。
|
||||||
cash = 0.0
|
cash = 0.0
|
||||||
total_assets = 0
|
total_assets = 0
|
||||||
market_value = 0
|
market_value = 0
|
||||||
|
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
for i, a in enumerate(args):
|
for i, a in enumerate(args):
|
||||||
if a == '--cash' and i + 1 < len(args):
|
if a == '--cash' and i + 1 < len(args):
|
||||||
cash = float(args[i + 1])
|
cash = float(args[i + 1])
|
||||||
elif a == '--total' and i + 1 < len(args):
|
elif a == '--total' and i + 1 < len(args):
|
||||||
total_assets = float(args[i + 1])
|
total_assets = float(args[i + 1])
|
||||||
elif a == '--mv' and i + 1 < len(args):
|
elif a == '--mv' and i + 1 < len(args):
|
||||||
market_value = float(args[i + 1])
|
market_value = float(args[i + 1])
|
||||||
|
|
||||||
with open(STOCKS_FILE, 'r', encoding='gbk') as f:
|
with open(STOCKS_FILE, 'r', encoding='gbk') as f:
|
||||||
reader = csv.reader(f, delimiter='\t')
|
reader = csv.reader(f, delimiter='\t')
|
||||||
rows = list(reader)
|
rows = list(reader)
|
||||||
|
|
||||||
print(f"读取 {STOCKS_FILE}: {len(rows)-1} 条记录")
|
print(f"读取 {STOCKS_FILE}: {len(rows)-1} 条记录")
|
||||||
|
|
||||||
holdings = []
|
holdings = []
|
||||||
total_mv_cny = 0
|
total_mv_cny = 0
|
||||||
|
|
||||||
for r in rows[1:]:
|
for r in rows[1:]:
|
||||||
code = clean_cell(r[0])
|
code = clean_cell(r[0])
|
||||||
name = r[1].strip()
|
name = r[1].strip()
|
||||||
shares = int(clean_cell(r[2]))
|
shares = int(clean_cell(r[2]))
|
||||||
# 跳过0股(已清仓的残留条目)
|
# 跳过0股(已清仓的残留条目)
|
||||||
if shares <= 0:
|
if shares <= 0:
|
||||||
continue
|
continue
|
||||||
price_raw = r[4].strip()
|
price_raw = r[4].strip()
|
||||||
currency = 'HKD' if '港币' in price_raw or '港' in r[10] else 'CNY'
|
currency = 'HKD' if '港币' in price_raw or '港' in r[10] else 'CNY'
|
||||||
price_str = price_raw.replace('港币', '').replace('港元', '').replace('港', '').strip()
|
price_str = price_raw.replace('港币', '').replace('港元', '').replace('港', '').strip()
|
||||||
price = float(price_str)
|
price = float(price_str)
|
||||||
cost_price = float(clean_cell(r[5]))
|
cost_price = float(clean_cell(r[5]))
|
||||||
pl = float(clean_cell(r[6])) if r[6].strip() else 0
|
pl = float(clean_cell(r[6])) if r[6].strip() else 0
|
||||||
mkt_val = float(clean_cell(r[11]))
|
mkt_val = float(clean_cell(r[11]))
|
||||||
cost_amount = float(clean_cell(r[15])) if r[15].strip() and r[15].strip() != '--' else 0
|
cost_amount = float(clean_cell(r[15])) if r[15].strip() and r[15].strip() != '--' else 0
|
||||||
rate_str = clean_cell(r[16])
|
rate_str = clean_cell(r[16])
|
||||||
rate = float(rate_str) if rate_str and rate_str != '--' else 0.8664
|
rate = float(rate_str) if rate_str and rate_str != '--' else 0.8664
|
||||||
mv_cny = mkt_val
|
mv_cny = mkt_val
|
||||||
total_mv_cny += mv_cny
|
total_mv_cny += mv_cny
|
||||||
|
|
||||||
holdings.append({
|
holdings.append({
|
||||||
'code': code, 'name': name, 'shares': shares,
|
'code': code, 'name': name, 'shares': shares,
|
||||||
'price': price, 'cost_price': round(cost_price, 2),
|
'price': price, 'cost_price': round(cost_price, 2),
|
||||||
'currency': currency, 'market_val': mkt_val,
|
'currency': currency, 'market_val': mkt_val,
|
||||||
'cost_amount': cost_amount, 'exchange_rate': rate,
|
'cost_amount': cost_amount, 'exchange_rate': rate,
|
||||||
})
|
})
|
||||||
|
|
||||||
if cash <= 0:
|
if cash <= 0:
|
||||||
print("⚠️ 警告:未提供现金(--cash),现金默认=0。Dad可能给了截图现金数!")
|
print("⚠️ 警告:未提供现金(--cash),现金默认=0。Dad可能给了截图现金数!")
|
||||||
print(" holding文件不含现金行,必须手动提供。可以用:")
|
print(" holding文件不含现金行,必须手动提供。可以用:")
|
||||||
print(f" python3 import_holding_xls.py --cash 73758.0")
|
print(f" python3 import_holding_xls.py --cash 73758.0")
|
||||||
|
|
||||||
# Use provided values or calculate (unified formula includes frozen_cash)
|
# Use provided values or calculate (unified formula includes frozen_cash)
|
||||||
if total_assets <= 0:
|
if total_assets <= 0:
|
||||||
frozen_cash = float(args.get('frozen', pf.get('frozen_cash', 0)) or 0)
|
frozen_cash = float(args.get('frozen', pf.get('frozen_cash', 0)) or 0)
|
||||||
total_assets = total_mv_cny + cash + frozen_cash
|
total_assets = total_mv_cny + cash + frozen_cash
|
||||||
if market_value <= 0:
|
if market_value <= 0:
|
||||||
market_value = round(total_mv_cny, 2)
|
market_value = round(total_mv_cny, 2)
|
||||||
|
|
||||||
position_pct = round(market_value / total_assets * 100, 2) if total_assets > 0 else 0
|
position_pct = round(market_value / total_assets * 100, 2) if total_assets > 0 else 0
|
||||||
|
|
||||||
# Step 1: Update SQLite (regenerate_all reads from here)
|
# Step 1: Update SQLite (regenerate_all reads from here)
|
||||||
print("\n→ 更新 SQLite holdings 表...")
|
print("\n→ 更新 SQLite holdings 表...")
|
||||||
conn = sqlite3.connect(DB_PATH)
|
conn = sqlite3.connect(DB_PATH)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('DELETE FROM holdings')
|
c.execute('DELETE FROM holdings')
|
||||||
c.execute('DELETE FROM portfolio_summary')
|
c.execute('DELETE FROM portfolio_summary')
|
||||||
for h in holdings:
|
for h in holdings:
|
||||||
c.execute('''
|
c.execute('''
|
||||||
INSERT INTO holdings (code, name, shares, cost, position_pct, added_at, is_active)
|
INSERT INTO holdings (code, name, shares, cost, position_pct, added_at, is_active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 1)
|
VALUES (?, ?, ?, ?, ?, ?, 1)
|
||||||
''', (h['code'], h['name'], h['shares'], h['cost_price'],
|
''', (h['code'], h['name'], h['shares'], h['cost_price'],
|
||||||
round(h['market_val'] / total_assets * 100, 2),
|
round(h['market_val'] / total_assets * 100, 2),
|
||||||
datetime.now().strftime('%Y-%m-%d')))
|
datetime.now().strftime('%Y-%m-%d')))
|
||||||
c.execute('''
|
c.execute('''
|
||||||
INSERT INTO portfolio_summary (total_assets, stock_value, cash, position_pct, total_pnl, updated_at)
|
INSERT INTO portfolio_summary (total_assets, stock_value, cash, position_pct, total_pnl, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
''', (round(total_assets, 2), round(market_value, 2), cash,
|
''', (round(total_assets, 2), round(market_value, 2), cash,
|
||||||
position_pct, 0, datetime.now().strftime('%Y-%m-%d %H:%M')))
|
position_pct, 0, datetime.now().strftime('%Y-%m-%d %H:%M')))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
print(f" OK - {len(holdings)} 只持仓")
|
print(f" OK - {len(holdings)} 只持仓")
|
||||||
|
|
||||||
# Step 2: Run full reassessment (reads SQLite, writes decisions.json + portfolio.json)
|
# Step 2: Run full reassessment (reads SQLite, writes decisions.json + portfolio.json)
|
||||||
print("\n→ 全量策略重评...")
|
print("\n→ 全量策略重评...")
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/per_stock_reassess.py"],
|
["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/per_stock_reassess.py"],
|
||||||
capture_output=True, text=True, timeout=120
|
capture_output=True, text=True, timeout=120
|
||||||
)
|
)
|
||||||
print(f" 完成")
|
print(f" 完成")
|
||||||
|
|
||||||
# Step 3: Overwrite portfolio.json with correct aggregate numbers
|
# Step 3: Overwrite portfolio.json with correct aggregate numbers
|
||||||
# (regenerate_all writes its own format, we fix it back)
|
# (regenerate_all writes its own format, we fix it back)
|
||||||
print("\n→ 修正 portfolio.json 汇总数据...")
|
print("\n→ 修正 portfolio.json 汇总数据...")
|
||||||
portfolio = {
|
portfolio = {
|
||||||
'holdings': holdings,
|
'holdings': holdings,
|
||||||
'cash': cash,
|
'cash': cash,
|
||||||
'total_market_value': market_value,
|
'total_market_value': market_value,
|
||||||
'total_assets': round(total_assets, 2),
|
'total_assets': round(total_assets, 2),
|
||||||
'total_pl': 0,
|
'total_pl': 0,
|
||||||
'position_pct': position_pct,
|
'position_pct': position_pct,
|
||||||
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
||||||
'source': STOCKS_FILE,
|
'source': STOCKS_FILE,
|
||||||
}
|
}
|
||||||
with open(PORTFOLIO_PATH, 'w') as f:
|
with open(PORTFOLIO_PATH, 'w') as f:
|
||||||
json.dump(portfolio, f, indent=2, ensure_ascii=False)
|
json.dump(portfolio, f, indent=2, ensure_ascii=False)
|
||||||
|
# DB 写入
|
||||||
# Step 4: Rebuild decision trees
|
try:
|
||||||
print("\n→ 重建决策树...")
|
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary
|
||||||
sys.path.insert(0, '/home/hmo/web-dashboard')
|
conn = get_conn()
|
||||||
from strategy_tree import init_default_branches
|
write_holdings_batch(conn, portfolio.get('holdings', []))
|
||||||
with open('/home/hmo/web-dashboard/data/decisions.json') as f:
|
write_portfolio_summary(conn, portfolio)
|
||||||
data = json.load(f)
|
conn.close()
|
||||||
ok = 0
|
except Exception as e:
|
||||||
for e in data.get('decisions', []):
|
print(f" [DB写入失败] {e}")
|
||||||
branches = init_default_branches(
|
|
||||||
e.get('code', ''), e.get('name', ''),
|
# Step 4: Rebuild decision trees
|
||||||
e.get('entry_low', 0), e.get('entry_high', 0),
|
print("\n→ 重建决策树...")
|
||||||
e.get('stop_loss', 0), e.get('take_profit', 0))
|
sys.path.insert(0, '/home/hmo/web-dashboard')
|
||||||
e['strategy_tree'] = {'branches': branches, 'created_at': datetime.now().strftime('%Y-%m-%d')}
|
from strategy_tree import init_default_branches
|
||||||
ok += 1
|
with open('/home/hmo/web-dashboard/data/decisions.json') as f:
|
||||||
with open('/home/hmo/web-dashboard/data/decisions.json', 'w') as f:
|
data = json.load(f)
|
||||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
ok = 0
|
||||||
|
for e in data.get('decisions', []):
|
||||||
print(f"\n{'='*50}")
|
branches = init_default_branches(
|
||||||
print(f"导入完成:{len(holdings)}只持仓")
|
e.get('code', ''), e.get('name', ''),
|
||||||
print(f"总资产: {round(total_assets):,.0f}元")
|
e.get('entry_low', 0), e.get('entry_high', 0),
|
||||||
print(f"市值: {market_value:,.0f}元")
|
e.get('stop_loss', 0), e.get('take_profit', 0))
|
||||||
print(f"现金: {cash:,.0f}元")
|
e['strategy_tree'] = {'branches': branches, 'created_at': datetime.now().strftime('%Y-%m-%d')}
|
||||||
print(f"仓位: {position_pct}%")
|
ok += 1
|
||||||
print(f"决策树: {ok}/{len(data.get('decisions',[]))}")
|
with open('/home/hmo/web-dashboard/data/decisions.json', 'w') as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
print(f"\n{'='*50}")
|
||||||
main()
|
print(f"导入完成:{len(holdings)}只持仓")
|
||||||
|
print(f"总资产: {round(total_assets):,.0f}元")
|
||||||
|
print(f"市值: {market_value:,.0f}元")
|
||||||
|
print(f"现金: {cash:,.0f}元")
|
||||||
|
print(f"仓位: {position_pct}%")
|
||||||
|
print(f"决策树: {ok}/{len(data.get('decisions',[]))}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|||||||
+148
-137
@@ -1,137 +1,148 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""process_trade.py — 处理交易截图,更新 portfolio.json
|
"""process_trade.py — 处理交易截图,更新 portfolio.json
|
||||||
|
|
||||||
Dad 发交易截图后,填入以下信息运行此脚本:
|
Dad 发交易截图后,填入以下信息运行此脚本:
|
||||||
python3 process_trade.py --action buy --code 600563 --shares 100 --price 189.20
|
python3 process_trade.py --action buy --code 600563 --shares 100 --price 189.20
|
||||||
|
|
||||||
它会:
|
它会:
|
||||||
1. 更新 holdings(加/减股数,归零则移除)
|
1. 更新 holdings(加/减股数,归零则移除)
|
||||||
2. 更新 cash(买入减现金,卖出加现金)
|
2. 更新 cash(买入减现金,卖出加现金)
|
||||||
3. 同步更新 decisions.json 的 shares 字段
|
3. 同步更新 decisions.json 的 shares 字段
|
||||||
4. 记录 changelog
|
4. 记录 changelog
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
python3 process_trade.py --action sell --code 600563 --shares 100 --price 189.20
|
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
|
python3 process_trade.py --action buy --code 300308 --shares 50 --price 1230.00
|
||||||
"""
|
"""
|
||||||
import json, sys, os
|
import json, sys, os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
|
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
|
||||||
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
|
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
args = {}
|
args = {}
|
||||||
for i, a in enumerate(sys.argv[1:]):
|
for i, a in enumerate(sys.argv[1:]):
|
||||||
if a.startswith("--"):
|
if a.startswith("--"):
|
||||||
key = a.lstrip("-")
|
key = a.lstrip("-")
|
||||||
val = sys.argv[i+2] if i+2 < len(sys.argv) and not sys.argv[i+2].startswith("--") else None
|
val = sys.argv[i+2] if i+2 < len(sys.argv) and not sys.argv[i+2].startswith("--") else None
|
||||||
args[key] = val
|
args[key] = val
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
action = args.get("action", "")
|
action = args.get("action", "")
|
||||||
code = args.get("code", "")
|
code = args.get("code", "")
|
||||||
shares = int(float(args.get("shares", 0)))
|
shares = int(float(args.get("shares", 0)))
|
||||||
price = float(args.get("price", 0))
|
price = float(args.get("price", 0))
|
||||||
name = args.get("name", "")
|
name = args.get("name", "")
|
||||||
|
|
||||||
if not action or not code or not shares or not price:
|
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")
|
print("用法: python3 process_trade.py --action sell --code 600563 --shares 100 --price 189.20")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
cost = shares * price
|
cost = shares * price
|
||||||
|
|
||||||
# 读数据
|
# 读数据
|
||||||
pf = json.load(open(PORTFOLIO_PATH))
|
pf = json.load(open(PORTFOLIO_PATH))
|
||||||
dec = json.load(open(DECISIONS_PATH))
|
dec = json.load(open(DECISIONS_PATH))
|
||||||
|
|
||||||
if action == "sell":
|
if action == "sell":
|
||||||
# 找持仓
|
# 找持仓
|
||||||
found = None
|
found = None
|
||||||
for h in pf["holdings"]:
|
for h in pf["holdings"]:
|
||||||
if h["code"] == code:
|
if h["code"] == code:
|
||||||
found = h
|
found = h
|
||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
print(f"❌ 错误: 代码 {code} 未在持仓中找到")
|
print(f"❌ 错误: 代码 {code} 未在持仓中找到")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
old_shares = found.get("shares", 0)
|
old_shares = found.get("shares", 0)
|
||||||
if old_shares < shares:
|
if old_shares < shares:
|
||||||
print(f"❌ 错误: 持仓只有 {old_shares} 股,不够卖 {shares} 股")
|
print(f"❌ 错误: 持仓只有 {old_shares} 股,不够卖 {shares} 股")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
# 减股数
|
# 减股数
|
||||||
found["shares"] = old_shares - shares
|
found["shares"] = old_shares - shares
|
||||||
found["updated_at"] = now
|
found["updated_at"] = now
|
||||||
# 归零则移除
|
# 归零则移除
|
||||||
if found["shares"] <= 0:
|
if found["shares"] <= 0:
|
||||||
pf["holdings"] = [h for h in pf["holdings"] if h["code"] != code]
|
pf["holdings"] = [h for h in pf["holdings"] if h["code"] != code]
|
||||||
print(f" 已全部清仓,从持仓移除")
|
print(f" 已全部清仓,从持仓移除")
|
||||||
# 加现金
|
# 加现金
|
||||||
old_cash = pf.get("cash", 0) or 0
|
old_cash = pf.get("cash", 0) or 0
|
||||||
pf["cash"] = round(old_cash + cost, 2)
|
pf["cash"] = round(old_cash + cost, 2)
|
||||||
print(f" ✅ 卖出 {name}({code}) {shares}股 @{price} = {cost:.2f}")
|
print(f" ✅ 卖出 {name}({code}) {shares}股 @{price} = {cost:.2f}")
|
||||||
print(f" 现金: {old_cash} → {pf['cash']}")
|
print(f" 现金: {old_cash} → {pf['cash']}")
|
||||||
|
|
||||||
elif action == "buy":
|
elif action == "buy":
|
||||||
# 找是否已有该股
|
# 找是否已有该股
|
||||||
found = None
|
found = None
|
||||||
for h in pf["holdings"]:
|
for h in pf["holdings"]:
|
||||||
if h["code"] == code:
|
if h["code"] == code:
|
||||||
found = h
|
found = h
|
||||||
break
|
break
|
||||||
if found:
|
if found:
|
||||||
# 加权平均成本
|
# 加权平均成本
|
||||||
old_shares = found.get("shares", 0) or 0
|
old_shares = found.get("shares", 0) or 0
|
||||||
old_cost = found.get("cost", 0) or 0
|
old_cost = found.get("cost", 0) or 0
|
||||||
total_cost = old_cost * old_shares + cost
|
total_cost = old_cost * old_shares + cost
|
||||||
new_shares = old_shares + shares
|
new_shares = old_shares + shares
|
||||||
new_avg_cost = round(total_cost / new_shares, 2) if new_shares > 0 else price
|
new_avg_cost = round(total_cost / new_shares, 2) if new_shares > 0 else price
|
||||||
found["shares"] = new_shares
|
found["shares"] = new_shares
|
||||||
found["cost"] = new_avg_cost
|
found["cost"] = new_avg_cost
|
||||||
found["updated_at"] = now
|
found["updated_at"] = now
|
||||||
else:
|
else:
|
||||||
pf["holdings"].append({
|
pf["holdings"].append({
|
||||||
"code": code, "name": name, "shares": shares,
|
"code": code, "name": name, "shares": shares,
|
||||||
"cost": price, "price": price, "updated_at": now
|
"cost": price, "price": price, "updated_at": now
|
||||||
})
|
})
|
||||||
# 减现金
|
# 减现金
|
||||||
old_cash = pf.get("cash", 0) or 0
|
old_cash = pf.get("cash", 0) or 0
|
||||||
pf["cash"] = round(old_cash - cost, 2)
|
pf["cash"] = round(old_cash - cost, 2)
|
||||||
print(f" ✅ 买入 {name}({code}) {shares}股 @{price} = {cost:.2f}")
|
print(f" ✅ 买入 {name}({code}) {shares}股 @{price} = {cost:.2f}")
|
||||||
print(f" 现金: {old_cash} → {pf['cash']}")
|
print(f" 现金: {old_cash} → {pf['cash']}")
|
||||||
|
|
||||||
# 同步 decisions.json 的 shares
|
# 同步 decisions.json 的 shares
|
||||||
for d in dec.get("decisions", []):
|
for d in dec.get("decisions", []):
|
||||||
if d["code"] == code:
|
if d["code"] == code:
|
||||||
old_dec_shares = d.get("shares", 0) or 0
|
old_dec_shares = d.get("shares", 0) or 0
|
||||||
d["shares"] = (d.get("shares", 0) or 0) + (shares if action == "buy" else -shares)
|
d["shares"] = (d.get("shares", 0) or 0) + (shares if action == "buy" else -shares)
|
||||||
if d["shares"] <= 0 and action == "sell":
|
if d["shares"] <= 0 and action == "sell":
|
||||||
d["shares"] = 0
|
d["shares"] = 0
|
||||||
d["type"] = "自选策略"
|
d["type"] = "自选策略"
|
||||||
d.setdefault("changelog", []).append({
|
d.setdefault("changelog", []).append({
|
||||||
"time": now,
|
"time": now,
|
||||||
"event": action,
|
"event": action,
|
||||||
"shares": shares,
|
"shares": shares,
|
||||||
"price": price,
|
"price": price,
|
||||||
"total": cost
|
"total": cost
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
# 写入
|
# 写入 — DB 优先
|
||||||
pf["updated_at"] = now
|
pf["updated_at"] = now
|
||||||
json.dump(pf, open(PORTFOLIO_PATH, "w"), indent=2, ensure_ascii=False)
|
try:
|
||||||
json.dump(dec, open(DECISIONS_PATH, "w"), indent=2, ensure_ascii=False)
|
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()
|
||||||
total_mv = sum((h.get("shares",0) or 0) * (h.get("price",0) or 0) for h in pf["holdings"])
|
write_holdings_batch(conn, pf.get('holdings', []))
|
||||||
total = round(total_mv + (pf.get("cash",0) or 0), 2)
|
write_portfolio_summary(conn, pf)
|
||||||
print(f"\n📊 持仓市值: {total_mv:.2f}")
|
for d in dec.get('decisions', []):
|
||||||
print(f"📊 现金: {pf.get('cash',0):.2f}")
|
write_holding_strategy(conn, d.get('code', ''), d.get('name', ''), d)
|
||||||
print(f"📊 总资产: {total:.2f}")
|
conn.close()
|
||||||
print(f"📊 持仓 {len(pf['holdings'])} 只")
|
except Exception:
|
||||||
|
pass
|
||||||
if __name__ == "__main__":
|
json.dump(pf, open(PORTFOLIO_PATH, "w"), indent=2, ensure_ascii=False)
|
||||||
main()
|
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