feat: holdings_reconciliation + process_trade + import_holding_xls → DB writes

This commit is contained in:
知微
2026-06-30 23:56:51 +08:00
parent e3bf196a59
commit 00b0e643d1
3 changed files with 471 additions and 440 deletions
+141 -130
View File
@@ -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
View File
@@ -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
View File
@@ -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()