fix(price_monitor): DB写锁死锁根治 - 统一BEGIN IMMEDIATE + 5次重试指数退避 + emergency WAL checkpoint
- 统一一个BEGIN IMMEDIATE事务包裹所有写操作,替代独立写函数调用 - 5次重试 + 指数退避 (1s,2s,4s,8s,16s),原3次+固定2s - 重试耗尽后自动 emergency WAL checkpoint(TRUNCATE)释放死锁 - try/except确保连接始终释放,修复conn泄漏 - 三副本同步:profile/scripts + MoFin/scripts + MoFin/root
This commit is contained in:
@@ -260,3 +260,16 @@ Bot被bash包装器手动启动(非systemd),形成孤儿进程。systemd
|
|||||||
- 每日汇总只发一次(16:35)
|
- 每日汇总只发一次(16:35)
|
||||||
- 健康监控新增数据实体检查
|
- 健康监控新增数据实体检查
|
||||||
- **涉及文件**: technical_analysis.py, mofin_health.py, cron_to_xmpp.py, cron_health_monitor.py
|
- **涉及文件**: technical_analysis.py, mofin_health.py, cron_to_xmpp.py, cron_health_monitor.py
|
||||||
|
|
||||||
|
## 2026-07-14 — price_monitor DB写锁死锁根治
|
||||||
|
|
||||||
|
### 根因
|
||||||
|
多 cron 脚本(price_monitor、mofin_health 等)同时 BEGIN IMMEDIATE 写 mofin.db,WAL 文件膨胀到 2.7MB 未 checkpoint,进程卡在 D 状态,后续全部超时。原代码的 3 次重试 + 2s 退避不足,且 write_portfolio_summary / write_live_prices 返回值未检查。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
1. **统一 BEGIN IMMEDIATE + 内联 SQL**(替代调用 write_holdings_batch/write_portfolio_summary/write_live_prices):一个事务包裹所有写操作,任一失败立即 rollback + 重试
|
||||||
|
2. **5 次重试 + 指数退避**(1s → 2s → 4s → 8s → 16s,共 ~31s),原 3 次 + 固定 2s
|
||||||
|
3. **Emergency WAL checkpoint**:所有重试耗尽后自动执行 PRAGMA wal_checkpoint(TRUNCATE) 释放死锁
|
||||||
|
4. **try/except 确保连接始终释放**(rollback + close),修复原代码异常时 conn 泄漏
|
||||||
|
5. **三副本同步**:profile/scripts + MoFin/scripts + MoFin/root
|
||||||
|
- **涉及文件**: scripts/price_monitor.py, /home/hmo/MoFin/scripts/price_monitor.py, /home/hmo/MoFin/price_monitor.py
|
||||||
|
|||||||
@@ -149,3 +149,38 @@ bash包装器启动bot会绕开systemd管理,导致:
|
|||||||
- 每日汇总只发一次(16:35 + sent标记防重复)
|
- 每日汇总只发一次(16:35 + sent标记防重复)
|
||||||
- cron_health_monitor新增数据实体完整性检查
|
- cron_health_monitor新增数据实体完整性检查
|
||||||
- 涉及文件: scripts/technical_analysis.py, scripts/strategy_lifecycle.py, scripts/mofin_health.py, scripts/cron_to_xmpp.py, scripts/cron_health_monitor.py
|
- 涉及文件: scripts/technical_analysis.py, scripts/strategy_lifecycle.py, scripts/mofin_health.py, scripts/cron_to_xmpp.py, scripts/cron_health_monitor.py
|
||||||
|
|
||||||
|
## 2026-07-14 — 系统检查:Dad未收到任何消息的诊断
|
||||||
|
|
||||||
|
### 发现问题
|
||||||
|
1. **cron_to_xmpp.py Bug**: body提取`content.split("## Response")`用`parts[1]`取到的是skill内容中的 ## Response(第一个),而不是agent实际回复的 ## Response(最后一个)。导致开盘简报等LLM报告的正文取错,被误判为[SILENT]静默拦截。
|
||||||
|
2. **price_monitor.py SQLite写锁死锁**: 多个cron脚本(price_monitor + mofin_health等)同时BEGIN IMMEDIATE写 mofin.db,进程卡在D状态,WAL文件膨胀到2.7MB,后续全部超时。
|
||||||
|
3. **mofin_db.py版本不同步**: cron版本和MoFin版本有差异(cron版本多了保留full_analysis/reassessed_at的代码)。
|
||||||
|
|
||||||
|
### 修改了什么
|
||||||
|
- `cron_to_xmpp.py` — 改 `parts[1]` → `parts[-1]`(取最后一个Response节)
|
||||||
|
- `mofin_db.py` (MoFin目录) — 同步cron版本的reassessed_at列处理和full_analysis保留逻辑
|
||||||
|
- `price_monitor.py` — 添加debug时序追踪,临时kill D状态进程+WAL checkpoint
|
||||||
|
- `.silent_daily_count.json` — 纠正静默计数(-1因为开盘简报被误拦)
|
||||||
|
|
||||||
|
### 文件
|
||||||
|
- Modified: /home/hmo/.hermes/profiles/position-analyst/scripts/cron_to_xmpp.py
|
||||||
|
- Modified: /home/hmo/MoFin/scripts/mofin_db.py
|
||||||
|
- Modified: /home/hmo/.hermes/profiles/position-analyst/scripts/price_monitor.py
|
||||||
|
|
||||||
|
## 2026-07-14 — price_monitor DB写锁死锁根治(跟进修复)
|
||||||
|
|
||||||
|
### 发现问题
|
||||||
|
上次诊断只做了临时修复(kill D状态进程 + WAL checkpoint),Dad要求直接根治。
|
||||||
|
|
||||||
|
### 修改了什么(三副本同步)
|
||||||
|
- **price_monitor.py** (profile/scripts): 统一 BEGIN IMMEDIATE 包裹所有写操作,替代调用 write_holdings_batch/write_portfolio_summary/write_live_prices。5次重试+指数退避(1s→2s→4s→8s→16s)。重试耗尽后自动 emergency WAL checkpoint。try/except 确保 conn 始终释放。
|
||||||
|
- **MoFin/scripts/price_monitor.py** — 同步修复
|
||||||
|
- **MoFin/price_monitor.py** — 同步修复(保留其 cash_log 优先的现金读取逻辑)
|
||||||
|
- **CHANGELOG.md** — 新增2026-07-14条目
|
||||||
|
|
||||||
|
### 文件
|
||||||
|
- Modified: /home/hmo/.hermes/profiles/position-analyst/scripts/price_monitor.py
|
||||||
|
- Modified: /home/hmo/MoFin/scripts/price_monitor.py
|
||||||
|
- Modified: /home/hmo/MoFin/price_monitor.py
|
||||||
|
- Modified: /home/hmo/MoFin/CHANGELOG.md
|
||||||
|
|||||||
+105
-39
@@ -23,7 +23,7 @@ EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json"
|
|||||||
# DB 模块(同步实时价到 mofin.db)
|
# DB 模块(同步实时价到 mofin.db)
|
||||||
sys.path.insert(0, "/home/hmo/MoFin")
|
sys.path.insert(0, "/home/hmo/MoFin")
|
||||||
try:
|
try:
|
||||||
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_live_prices
|
from mofin_db import get_conn, DB_PATH
|
||||||
from mo_models import calc_total_mv, calc_total_assets
|
from mo_models import calc_total_mv, calc_total_assets
|
||||||
from mo_data import read_decisions
|
from mo_data import read_decisions
|
||||||
HAS_DB = True
|
HAS_DB = True
|
||||||
@@ -125,13 +125,32 @@ def refresh_data_prices():
|
|||||||
prices = fetch_all_prices(list(all_codes))
|
prices = fetch_all_prices(list(all_codes))
|
||||||
updated = len(prices)
|
updated = len(prices)
|
||||||
|
|
||||||
# === 同步实时价到 mofin.db(带重试防锁) ===
|
# === 弹性同步实时价到 mofin.db ===
|
||||||
|
# 防死锁策略(经2026-07-14 WAL死锁复盘改进):
|
||||||
|
# ① 启动时 checkpoint WAL(清理残留事务)
|
||||||
|
# ② 统一 BEGIN IMMEDIATE 包裹整个写操作
|
||||||
|
# ③ 5次重试 + 指数退避: 1s → 2s → 4s → 8s → 16s(共~31s)
|
||||||
|
# ④ get_conn() 的 busy_timeout=30000 保证等待上限
|
||||||
|
# ⑤ 每个写操作检查返回值,任一失败立即 rollback + 重试
|
||||||
|
# ⑥ try/finally 确保连接始终释放
|
||||||
if HAS_DB and prices:
|
if HAS_DB and prices:
|
||||||
for db_attempt in range(3):
|
# 先checkpoint一次,清理上次被kill残留的WAL
|
||||||
|
try:
|
||||||
|
c = get_conn()
|
||||||
|
c.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||||
|
c.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
max_tries = 5
|
||||||
|
conn = None
|
||||||
|
for db_attempt in range(max_tries):
|
||||||
try:
|
try:
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
|
# BEGIN IMMEDIATE 立即获取写锁——失败则等 busy_timeout(30s)
|
||||||
|
conn.execute("BEGIN IMMEDIATE")
|
||||||
|
|
||||||
# 构建 holdings 更新数据
|
# ── 构建 holdings 更新数据 ──
|
||||||
db_holdings = []
|
db_holdings = []
|
||||||
for r in conn.execute("SELECT * FROM holdings WHERE is_active=1"):
|
for r in conn.execute("SELECT * FROM holdings WHERE is_active=1"):
|
||||||
h = dict(r)
|
h = dict(r)
|
||||||
@@ -143,20 +162,28 @@ def refresh_data_prices():
|
|||||||
h['change_pct'] = float(change_pct) if change_pct else 0
|
h['change_pct'] = float(change_pct) if change_pct else 0
|
||||||
db_holdings.append(h)
|
db_holdings.append(h)
|
||||||
|
|
||||||
# 写入DB持仓价格(write_holdings_batch 用 ON CONFLICT UPDATE 只改价格字段)
|
# ── 写 holdings 表 ──
|
||||||
ok, msg = write_holdings_batch(conn, db_holdings)
|
for h in db_holdings:
|
||||||
if not ok:
|
currency = str(h.get('currency', 'CNY')).upper()
|
||||||
conn.close()
|
if currency not in ('CNY', 'HKD'):
|
||||||
if db_attempt < 2:
|
raise ValueError(f"非法币种: {currency}")
|
||||||
wait = (db_attempt + 1) * 2
|
conn.execute("""
|
||||||
print(f"⏳ DB写持仓失败: {msg} → {wait}s后重试", file=sys.stderr)
|
INSERT INTO holdings (code, name, shares, cost, price, market_value,
|
||||||
time.sleep(wait)
|
change_pct, currency, position_pct, added_at, is_active)
|
||||||
continue
|
VALUES (?,?,?,?,?,?,?,?,?,datetime('now','localtime'),1)
|
||||||
else:
|
ON CONFLICT(code) DO UPDATE SET
|
||||||
print(f"❌ DB写持仓失败(3次重试耗尽): {msg}", file=sys.stderr)
|
name=excluded.name, shares=excluded.shares, cost=excluded.cost,
|
||||||
break
|
price=excluded.price, market_value=excluded.market_value,
|
||||||
|
change_pct=excluded.change_pct, currency=excluded.currency,
|
||||||
|
position_pct=excluded.position_pct
|
||||||
|
""", (
|
||||||
|
h.get('code'), h.get('name'), h.get('shares', 0),
|
||||||
|
h.get('cost'), h.get('price'),
|
||||||
|
h.get('market_value'), h.get('change_pct'),
|
||||||
|
h.get('currency', 'CNY'), h.get('position_pct'),
|
||||||
|
))
|
||||||
|
|
||||||
# 重新计算市值(现金从cash_log取最新Dad确认值——price_monitor不再持有现金权威)
|
# ── 写 portfolio_summary ──
|
||||||
mv = calc_total_mv(db_holdings)
|
mv = calc_total_mv(db_holdings)
|
||||||
# 从cash_log读取最新verified现金(Dad确认的才是权威),不读portfolio_summary
|
# 从cash_log读取最新verified现金(Dad确认的才是权威),不读portfolio_summary
|
||||||
latest = conn.execute(
|
latest = conn.execute(
|
||||||
@@ -167,7 +194,6 @@ def refresh_data_prices():
|
|||||||
db_cash = latest['cash_after'] or 0.0
|
db_cash = latest['cash_after'] or 0.0
|
||||||
db_frozen = latest['frozen_after'] or 0.0
|
db_frozen = latest['frozen_after'] or 0.0
|
||||||
else:
|
else:
|
||||||
# 首次运行/无cash_log记录时回退到summary现存值
|
|
||||||
existing = conn.execute(
|
existing = conn.execute(
|
||||||
'SELECT cash, frozen_cash FROM portfolio_summary WHERE id=1'
|
'SELECT cash, frozen_cash FROM portfolio_summary WHERE id=1'
|
||||||
).fetchone()
|
).fetchone()
|
||||||
@@ -175,36 +201,76 @@ def refresh_data_prices():
|
|||||||
db_frozen = existing['frozen_cash'] if existing else 0.0
|
db_frozen = existing['frozen_cash'] if existing else 0.0
|
||||||
assets = calc_total_assets({'holdings': db_holdings, 'cash': db_cash, 'frozen_cash': db_frozen})
|
assets = calc_total_assets({'holdings': db_holdings, 'cash': db_cash, 'frozen_cash': db_frozen})
|
||||||
position_pct = round(mv / assets * 100, 2) if assets > 0 else 0
|
position_pct = round(mv / assets * 100, 2) if assets > 0 else 0
|
||||||
write_portfolio_summary(conn, {
|
conn.execute("""
|
||||||
'total_mv': mv,
|
INSERT INTO portfolio_summary (id, total_assets, total_mv, stock_value,
|
||||||
'total_assets': assets,
|
cash, frozen_cash, position_pct, total_pnl, currency, updated_at)
|
||||||
'stock_value': mv,
|
VALUES (1,?,?,?,?,?,?,?,?,datetime('now','localtime'))
|
||||||
'cash': db_cash,
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
'frozen_cash': db_frozen,
|
total_assets=excluded.total_assets, total_mv=excluded.total_mv,
|
||||||
'position_pct': position_pct,
|
stock_value=excluded.stock_value, cash=excluded.cash,
|
||||||
'currency': 'CNY',
|
frozen_cash=excluded.frozen_cash, position_pct=excluded.position_pct,
|
||||||
})
|
total_pnl=excluded.total_pnl, currency=excluded.currency,
|
||||||
# 写实时价格表(供 read_live_prices 消费)
|
updated_at=datetime('now','localtime')
|
||||||
live = {h['code']: {'price': h.get('price',0), 'change_pct': h.get('change_pct',0)}
|
""", (
|
||||||
for h in db_holdings if h.get('code')}
|
assets, mv, mv, db_cash, db_frozen,
|
||||||
write_live_prices(conn, live)
|
position_pct, 0, 'CNY',
|
||||||
|
))
|
||||||
|
|
||||||
|
# ── 写 live_prices ──
|
||||||
|
for h in db_holdings:
|
||||||
|
code = h.get('code', '')
|
||||||
|
if code:
|
||||||
|
p = h.get('price', 0)
|
||||||
|
cp = h.get('change_pct', 0)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO live_prices (code, price, change_pct, updated_at) "
|
||||||
|
"VALUES (?,?,?,datetime('now','localtime'))",
|
||||||
|
(code, p, cp)
|
||||||
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
conn = None
|
||||||
if db_attempt > 0:
|
if db_attempt > 0:
|
||||||
print(f"DB同步成功(第{db_attempt+1}次重试)")
|
print(f"DB同步成功(第{db_attempt+1}次重试)")
|
||||||
break # success
|
break # success
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
conn.close()
|
except (sqlite3.OperationalError, sqlite3.DatabaseError) as e:
|
||||||
if db_attempt < 2:
|
if conn:
|
||||||
wait = (db_attempt + 1) * 2
|
try: conn.rollback()
|
||||||
print(f"⏳ DB锁等待(第{db_attempt+1}次): {e} → {wait}s后重试", file=sys.stderr)
|
except Exception: pass
|
||||||
time.sleep(wait)
|
try: conn.close()
|
||||||
|
except Exception: pass
|
||||||
|
conn = None
|
||||||
|
err_str = str(e)
|
||||||
|
if "locked" in err_str or "cannot commit" in err_str or "busy" in err_str:
|
||||||
|
if db_attempt < max_tries - 1:
|
||||||
|
wait = 2 ** db_attempt # 1, 2, 4, 8, 16
|
||||||
|
print(f"⏳ DB锁(尝试{db_attempt+1}/{max_tries}): {e} → {wait}s后重试", file=sys.stderr)
|
||||||
|
time.sleep(wait)
|
||||||
|
else:
|
||||||
|
print(f"❌ DB锁(重试{max_tries}次耗尽): {e}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"❌ DB同步失败(3次重试耗尽): {e}", file=sys.stderr)
|
print(f"❌ DB错误: {e}", file=sys.stderr)
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.close()
|
if conn:
|
||||||
|
try: conn.rollback()
|
||||||
|
except Exception: pass
|
||||||
|
try: conn.close()
|
||||||
|
except Exception: pass
|
||||||
|
conn = None
|
||||||
print(f"⚠️ DB同步异常: {e}", file=sys.stderr)
|
print(f"⚠️ DB同步异常: {e}", file=sys.stderr)
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
print("❌ DB同步失败(所有重试耗尽)", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
c = sqlite3.connect(str(DB_PATH), timeout=1)
|
||||||
|
c.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||||
|
c.close()
|
||||||
|
print(" ↪ 紧急WAL checkpoint完成", file=sys.stderr)
|
||||||
|
except Exception as we:
|
||||||
|
print(f" ↪ WAL checkpoint也失败: {we}", file=sys.stderr)
|
||||||
|
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|||||||
+116
-43
@@ -17,7 +17,7 @@ EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json"
|
|||||||
# DB 模块(同步实时价到 mofin.db)
|
# DB 模块(同步实时价到 mofin.db)
|
||||||
sys.path.insert(0, "/home/hmo/MoFin")
|
sys.path.insert(0, "/home/hmo/MoFin")
|
||||||
try:
|
try:
|
||||||
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_live_prices
|
from mofin_db import get_conn, DB_PATH
|
||||||
from mo_models import calc_total_mv, calc_total_assets
|
from mo_models import calc_total_mv, calc_total_assets
|
||||||
HAS_DB = True
|
HAS_DB = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -139,13 +139,32 @@ def refresh_data_prices():
|
|||||||
prices = fetch_all_prices(list(all_codes))
|
prices = fetch_all_prices(list(all_codes))
|
||||||
updated = len(prices)
|
updated = len(prices)
|
||||||
|
|
||||||
# === 同步实时价到 mofin.db(带重试防锁) ===
|
# === 弹性同步实时价到 mofin.db ===
|
||||||
|
# 防死锁策略(经2026-07-14 WAL死锁复盘改进):
|
||||||
|
# ① 启动时 checkpoint WAL(清理残留事务)
|
||||||
|
# ② 统一 BEGIN IMMEDIATE 包裹整个写操作
|
||||||
|
# ③ 5次重试 + 指数退避: 1s → 2s → 4s → 8s → 16s(共~31s)
|
||||||
|
# ④ get_conn() 的 busy_timeout=30000 保证等待上限
|
||||||
|
# ⑤ 每个写操作检查返回值,任一失败立即 rollback + 重试
|
||||||
|
# ⑥ try/finally 确保连接始终释放
|
||||||
if HAS_DB and prices:
|
if HAS_DB and prices:
|
||||||
for db_attempt in range(3):
|
# 先checkpoint一次,清理上次被kill残留的WAL
|
||||||
|
try:
|
||||||
|
c = get_conn()
|
||||||
|
c.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||||
|
c.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
max_tries = 5
|
||||||
|
conn = None
|
||||||
|
for db_attempt in range(max_tries):
|
||||||
try:
|
try:
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
|
# BEGIN IMMEDIATE 立即获取写锁——失败则等 busy_timeout(30s)
|
||||||
|
conn.execute("BEGIN IMMEDIATE")
|
||||||
|
|
||||||
# 构建 holdings 更新数据
|
# ── 构建 holdings 更新数据 ──
|
||||||
db_holdings = []
|
db_holdings = []
|
||||||
for r in conn.execute("SELECT * FROM holdings WHERE is_active=1"):
|
for r in conn.execute("SELECT * FROM holdings WHERE is_active=1"):
|
||||||
h = dict(r)
|
h = dict(r)
|
||||||
@@ -157,22 +176,29 @@ def refresh_data_prices():
|
|||||||
h['change_pct'] = float(change_pct) if change_pct else 0
|
h['change_pct'] = float(change_pct) if change_pct else 0
|
||||||
db_holdings.append(h)
|
db_holdings.append(h)
|
||||||
|
|
||||||
# 写入DB持仓价格(write_holdings_batch 用 ON CONFLICT UPDATE 只改价格字段)
|
# ── 写 holdings 表 ──
|
||||||
ok, msg = write_holdings_batch(conn, db_holdings)
|
for h in db_holdings:
|
||||||
if not ok:
|
currency = str(h.get('currency', 'CNY')).upper()
|
||||||
conn.close()
|
if currency not in ('CNY', 'HKD'):
|
||||||
if db_attempt < 2:
|
raise ValueError(f"非法币种: {currency}")
|
||||||
wait = (db_attempt + 1) * 2
|
conn.execute("""
|
||||||
print(f"⏳ DB写持仓失败: {msg} → {wait}s后重试", file=sys.stderr)
|
INSERT INTO holdings (code, name, shares, cost, price, market_value,
|
||||||
time.sleep(wait)
|
change_pct, currency, position_pct, added_at, is_active)
|
||||||
continue
|
VALUES (?,?,?,?,?,?,?,?,?,datetime('now','localtime'),1)
|
||||||
else:
|
ON CONFLICT(code) DO UPDATE SET
|
||||||
print(f"❌ DB写持仓失败(3次重试耗尽): {msg}", file=sys.stderr)
|
name=excluded.name, shares=excluded.shares, cost=excluded.cost,
|
||||||
break
|
price=excluded.price, market_value=excluded.market_value,
|
||||||
|
change_pct=excluded.change_pct, currency=excluded.currency,
|
||||||
|
position_pct=excluded.position_pct
|
||||||
|
""", (
|
||||||
|
h.get('code'), h.get('name'), h.get('shares', 0),
|
||||||
|
h.get('cost'), h.get('price'),
|
||||||
|
h.get('market_value'), h.get('change_pct'),
|
||||||
|
h.get('currency', 'CNY'), h.get('position_pct'),
|
||||||
|
))
|
||||||
|
|
||||||
# 重新计算市值(不变现金——DB的cash是权威)
|
# ── 写 portfolio_summary ──
|
||||||
mv = calc_total_mv(db_holdings)
|
mv = calc_total_mv(db_holdings)
|
||||||
# 读取DB当前的现金和冻结,不覆盖
|
|
||||||
existing = conn.execute(
|
existing = conn.execute(
|
||||||
'SELECT cash, frozen_cash FROM portfolio_summary WHERE id=1'
|
'SELECT cash, frozen_cash FROM portfolio_summary WHERE id=1'
|
||||||
).fetchone()
|
).fetchone()
|
||||||
@@ -180,41 +206,88 @@ def refresh_data_prices():
|
|||||||
db_frozen = existing['frozen_cash'] if existing else 0.0
|
db_frozen = existing['frozen_cash'] if existing else 0.0
|
||||||
assets = calc_total_assets({'holdings': db_holdings, 'cash': db_cash, 'frozen_cash': db_frozen})
|
assets = calc_total_assets({'holdings': db_holdings, 'cash': db_cash, 'frozen_cash': db_frozen})
|
||||||
position_pct = round(mv / assets * 100, 2) if assets > 0 else 0
|
position_pct = round(mv / assets * 100, 2) if assets > 0 else 0
|
||||||
write_portfolio_summary(conn, {
|
conn.execute("""
|
||||||
'total_mv': mv,
|
INSERT INTO portfolio_summary (id, total_assets, total_mv, stock_value,
|
||||||
'total_assets': assets,
|
cash, frozen_cash, position_pct, total_pnl, currency, updated_at)
|
||||||
'stock_value': mv,
|
VALUES (1,?,?,?,?,?,?,?,?,datetime('now','localtime'))
|
||||||
'cash': db_cash,
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
'frozen_cash': db_frozen,
|
total_assets=excluded.total_assets, total_mv=excluded.total_mv,
|
||||||
'position_pct': position_pct,
|
stock_value=excluded.stock_value, cash=excluded.cash,
|
||||||
'currency': 'CNY',
|
frozen_cash=excluded.frozen_cash, position_pct=excluded.position_pct,
|
||||||
})
|
total_pnl=excluded.total_pnl, currency=excluded.currency,
|
||||||
# 写实时价格表(供 read_live_prices 消费)
|
updated_at=datetime('now','localtime')
|
||||||
live = {h['code']: {'price': h.get('price',0), 'change_pct': h.get('change_pct',0)}
|
""", (
|
||||||
for h in db_holdings if h.get('code')}
|
assets, mv, mv, db_cash, db_frozen,
|
||||||
# 补充自选股/策略股的价格(它们不在holdings表中)
|
position_pct, 0, 'CNY',
|
||||||
|
))
|
||||||
|
|
||||||
|
# ── 写 live_prices ──
|
||||||
|
for h in db_holdings:
|
||||||
|
code = h.get('code', '')
|
||||||
|
if code:
|
||||||
|
p = h.get('price', 0)
|
||||||
|
cp = h.get('change_pct', 0)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO live_prices (code, price, change_pct, updated_at) "
|
||||||
|
"VALUES (?,?,?,datetime('now','localtime'))",
|
||||||
|
(code, p, cp)
|
||||||
|
)
|
||||||
|
# 补充策略股/自选股的价格(不在holdings中的)
|
||||||
for code, pdata in prices.items():
|
for code, pdata in prices.items():
|
||||||
if code not in live:
|
if code not in {h.get('code') for h in db_holdings}:
|
||||||
live[code] = {'price': pdata[0] if isinstance(pdata, (list, tuple)) else pdata.get('price',0),
|
price_val = pdata[0] if isinstance(pdata, (list, tuple)) else pdata.get('price', 0)
|
||||||
'change_pct': pdata[1] if isinstance(pdata, (list, tuple)) else pdata.get('change_pct',0)}
|
cp_val = pdata[1] if isinstance(pdata, (list, tuple)) else pdata.get('change_pct', 0)
|
||||||
write_live_prices(conn, live)
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO live_prices (code, price, change_pct, updated_at) "
|
||||||
|
"VALUES (?,?,?,datetime('now','localtime'))",
|
||||||
|
(code, price_val, cp_val)
|
||||||
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
conn = None
|
||||||
if db_attempt > 0:
|
if db_attempt > 0:
|
||||||
print(f"DB同步成功(第{db_attempt+1}次重试)")
|
print(f"DB同步成功(第{db_attempt+1}次重试)")
|
||||||
break # success
|
break # success
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
conn.close()
|
except (sqlite3.OperationalError, sqlite3.DatabaseError) as e:
|
||||||
if db_attempt < 2:
|
if conn:
|
||||||
wait = (db_attempt + 1) * 2
|
try: conn.rollback()
|
||||||
print(f"⏳ DB锁等待(第{db_attempt+1}次): {e} → {wait}s后重试", file=sys.stderr)
|
except Exception: pass
|
||||||
time.sleep(wait)
|
try: conn.close()
|
||||||
|
except Exception: pass
|
||||||
|
conn = None
|
||||||
|
err_str = str(e)
|
||||||
|
if "locked" in err_str or "cannot commit" in err_str or "busy" in err_str:
|
||||||
|
if db_attempt < max_tries - 1:
|
||||||
|
wait = 2 ** db_attempt # 1, 2, 4, 8, 16
|
||||||
|
print(f"⏳ DB锁(尝试{db_attempt+1}/{max_tries}): {e} → {wait}s后重试", file=sys.stderr)
|
||||||
|
time.sleep(wait)
|
||||||
|
else:
|
||||||
|
print(f"❌ DB锁(重试{max_tries}次耗尽): {e}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"❌ DB同步失败(3次重试耗尽): {e}", file=sys.stderr)
|
print(f"❌ DB错误: {e}", file=sys.stderr)
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.close()
|
if conn:
|
||||||
|
try: conn.rollback()
|
||||||
|
except Exception: pass
|
||||||
|
try: conn.close()
|
||||||
|
except Exception: pass
|
||||||
|
conn = None
|
||||||
print(f"⚠️ DB同步异常: {e}", file=sys.stderr)
|
print(f"⚠️ DB同步异常: {e}", file=sys.stderr)
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
# for-else: loop exhausted without break
|
||||||
|
print("❌ DB同步失败(所有重试耗尽)", file=sys.stderr)
|
||||||
|
# 尝试紧急 WAL checkpoint(释放死锁)
|
||||||
|
try:
|
||||||
|
c = sqlite3.connect(str(DB_PATH), timeout=1)
|
||||||
|
c.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||||
|
c.close()
|
||||||
|
print(" ↪ 紧急WAL checkpoint完成", file=sys.stderr)
|
||||||
|
except Exception as we:
|
||||||
|
print(f" ↪ WAL checkpoint也失败: {we}", file=sys.stderr)
|
||||||
|
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user