feat: DB-first architecture with lock-safe writes
- price_monitor: writes live prices to both JSON and mofin.db (holdings + live_prices + portfolio_summary) - mofin_db: added execute_with_retry/commit_with_retry with exponential backoff on 'database is locked' - mofin_db: increased timeout 5s->15s, added PRAGMA busy_timeout=15000 - price_monitor retry loop: fixed break-before-if-ok bug (was not retrying on write failure) - DB connection: WAL mode + retry decorator for all write operations - cash sync: preserves DB authoritative cash (JSON cash not pushed to DB) This is the DB-first version. JSON writes remain for dashboard compatibility. Next step: remove JSON writes entirely for full DB-only architecture.
This commit is contained in:
+47
-8
@@ -20,15 +20,22 @@ DB_PATH = '/home/hmo/web-dashboard/data/mofin.db'
|
||||
|
||||
|
||||
def _get_db():
|
||||
db = sqlite3.connect(DB_PATH)
|
||||
"""获取数据库连接(WAL 模式,15秒超时防并发锁)"""
|
||||
db = sqlite3.connect(DB_PATH, timeout=15)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute("PRAGMA journal_mode=WAL")
|
||||
db.execute("PRAGMA busy_timeout=15000")
|
||||
return db
|
||||
|
||||
|
||||
# ── portfolio ─────────────────────────────────────────────────────
|
||||
|
||||
def read_portfolio():
|
||||
"""返回 portfolio.json 等价 dict。纯 DB。"""
|
||||
"""返回 portfolio.json 等价 dict。纯 DB。
|
||||
|
||||
总市值从 holdings 实时计算(shares × price,港股 × 汇率),
|
||||
不信任 portfolio_summary 的存储值,因为可能未及时更新。
|
||||
"""
|
||||
db = _get_db()
|
||||
rows = db.execute(
|
||||
"SELECT code, name, shares, cost, price, market_value, "
|
||||
@@ -46,14 +53,46 @@ def read_portfolio():
|
||||
|
||||
db.close()
|
||||
|
||||
# ── 总市值从 holdings 实时计算 ────────────────────
|
||||
cash = float(summary.get('cash', 0) or 0)
|
||||
frozen = float(summary.get('frozen_cash', 0) or 0)
|
||||
|
||||
# 获取港股汇率
|
||||
import subprocess, os as _os
|
||||
rate = 0.865 # 默认值
|
||||
try:
|
||||
hk_rate_py = _os.path.join(_os.path.dirname(_os.path.dirname(__file__)), 'hk_rate.py')
|
||||
if _os.path.exists(hk_rate_py):
|
||||
r = subprocess.run(
|
||||
[_os.path.join(_os.path.dirname(_os.path.dirname(__file__)), 'venv', 'bin', 'python3'),
|
||||
hk_rate_py, '--rate'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
rate = float(r.stdout.strip())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
total_mv = 0.0
|
||||
for h in holdings:
|
||||
p = float(h.get('price', 0) or 0)
|
||||
s = float(h.get('shares', 0) or 0)
|
||||
if h.get('currency') == 'HKD' or (len(str(h.get('code',''))) == 5 and str(h.get('code',''))[0] in ('0','1')):
|
||||
total_mv += s * p * rate
|
||||
else:
|
||||
total_mv += s * p
|
||||
total_mv = round(total_mv, 2)
|
||||
total_assets = round(total_mv + cash + frozen, 2)
|
||||
position_pct = round(total_mv / total_assets * 100, 2) if total_assets > 0 else 0
|
||||
|
||||
return {
|
||||
"holdings": holdings,
|
||||
"total_assets": summary.get("total_assets", 0),
|
||||
"total_mv": summary.get("total_mv", 0),
|
||||
"stock_value": summary.get("stock_value", summary.get("total_mv", 0)),
|
||||
"cash": summary.get("cash", 0),
|
||||
"frozen_cash": summary.get("frozen_cash", 0),
|
||||
"position_pct": summary.get("position_pct", 0),
|
||||
"total_assets": total_assets,
|
||||
"total_mv": total_mv,
|
||||
"stock_value": total_mv,
|
||||
"cash": cash,
|
||||
"frozen_cash": frozen,
|
||||
"position_pct": position_pct,
|
||||
"currency": summary.get("currency", "CNY"),
|
||||
"updated_at": summary.get("updated_at", ""),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user