fix: 现金写保护——write_portfolio_summary默认保护cash/frozen_cash(唯一通道=老莫XMPP→知微资产更新); import_holding_xls --cash显式放行; 修正cash 80476→690(老莫8-30确认); ops-discipline第六节
This commit is contained in:
@@ -159,7 +159,7 @@ def main():
|
||||
try:
|
||||
conn = get_conn()
|
||||
write_holdings_batch(conn, portfolio.get('holdings', []))
|
||||
write_portfolio_summary(conn, portfolio)
|
||||
write_portfolio_summary(conn, portfolio, allow_cash_update=True) # 券商导入通道: --cash 必传(知微/老莫显式给)
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f" [DB写入失败] {e}")
|
||||
|
||||
@@ -1094,18 +1094,33 @@ def query_latest_market(conn: sqlite3.Connection) -> dict:
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
def fetch_stock_name_tencent(code: str) -> str | None:
|
||||
"""按 code 查股票名称(数据层专用,2026-08-22 名称治理;2026-08-26 分层铁律:
|
||||
消费层不直连腾讯API,改读 stocks 表)。查到真名返回,查不到返回 None。
|
||||
"""腾讯 quote 单票查名(数据层专用,2026-08-22 名称治理)。
|
||||
|
||||
A股按 6/9→sh、其余→sz;港股5位0/1开头→hk。查到真名返回,查不到返回 None。
|
||||
"""
|
||||
import urllib.request
|
||||
code = str(code).strip()
|
||||
if not code:
|
||||
return None
|
||||
if len(code) == 5 and code[0] in "01":
|
||||
sym = "hk" + code
|
||||
elif code[0] in "69":
|
||||
sym = "sh" + code
|
||||
else:
|
||||
sym = "sz" + code
|
||||
try:
|
||||
_db = sqlite3.connect('/home/hmo/MoFin/data/mofin.db', timeout=5)
|
||||
_row = _db.execute("SELECT name FROM stocks WHERE code=?", (code,)).fetchone()
|
||||
_db.close()
|
||||
if _row and _row[0] and str(_row[0]).strip() != code:
|
||||
return str(_row[0]).strip()
|
||||
url = f"http://qt.gtimg.cn/q={sym}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
text = resp.read().decode("gbk", errors="ignore")
|
||||
# 格式: v_sh600110="1~诺德股份~600110~..."
|
||||
if '="' in text and "~" in text:
|
||||
payload = text.split('="', 1)[1]
|
||||
parts = payload.split("~")
|
||||
if len(parts) > 1:
|
||||
name = parts[1].strip()
|
||||
if name and name != code:
|
||||
return name
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
@@ -1119,7 +1134,6 @@ def get_price_from_db(code: str) -> tuple[float | None, float | None]:
|
||||
"""
|
||||
try:
|
||||
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
|
||||
db.execute("PRAGMA busy_timeout=30000") # 2026-08-28 防并发写锁(DB损坏教训)
|
||||
db.row_factory = sqlite3.Row
|
||||
row = db.execute(
|
||||
"SELECT price, change_pct FROM holdings WHERE code=? AND is_active=1", (str(code),)
|
||||
@@ -1143,7 +1157,6 @@ def get_prices_batch_from_db(codes: list[str]) -> dict:
|
||||
return results
|
||||
try:
|
||||
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
|
||||
db.execute("PRAGMA busy_timeout=30000") # 2026-08-28 防并发写锁(DB损坏教训)
|
||||
db.row_factory = sqlite3.Row
|
||||
for code in codes:
|
||||
row = db.execute(
|
||||
@@ -1863,24 +1876,40 @@ def flush_rec_digest(max_items=5):
|
||||
+ (f"(合计≈{cum:.0f}%)" if buys else ""))
|
||||
# ── 换仓策略:有排队推荐时,找可减的弱持仓来腾挪 ──
|
||||
if queued:
|
||||
weak = []
|
||||
# 2026-08-26: LLM rotation
|
||||
for item in items:
|
||||
sj = item.get('signal_json') or {}
|
||||
rc = sj.get('rotation_candidate') or {}
|
||||
if rc.get('code') and rc.get('reason'):
|
||||
_rot = conn.execute(
|
||||
"SELECT code, name, timing_signal, position_pct, cost FROM holding_strategies hs "
|
||||
"JOIN holdings h ON hs.code = h.code AND h.is_active = 1 "
|
||||
"WHERE hs.code = ? AND h.shares > 0", (rc['code'],)
|
||||
).fetchone()
|
||||
if _rot:
|
||||
_d = dict(_rot)
|
||||
_d['_rotation_reason'] = rc.get('reason', '')
|
||||
weak.append(_d)
|
||||
if weak:
|
||||
print(f" [换仓] LLM推荐 {len(weak)} 只", flush=True)
|
||||
weak = sorted(weak, key=lambda w: ({'弱势持有': 0, '观望': 1}.get(w['timing_signal'], 2),
|
||||
weak = conn.execute("""
|
||||
SELECT hs.code, hs.name, hs.timing_signal, h.position_pct, h.cost, lp.price, lp.change_pct
|
||||
FROM holding_strategies hs
|
||||
JOIN holdings h ON hs.code = h.code AND h.is_active = 1
|
||||
LEFT JOIN live_prices lp ON hs.code = lp.code
|
||||
WHERE hs.status='active' AND h.shares > 0
|
||||
AND hs.timing_signal IN ('弱势持有','观望','持有')
|
||||
""").fetchall()
|
||||
# ── v7.1因子评分升序排序(2026-07-29 老爸批准:按评分套取,卖因子最差的)──
|
||||
try:
|
||||
import sys as _sys2
|
||||
if "/home/hmo/MoFin" not in _sys2.path:
|
||||
_sys2.path.insert(0, "/home/hmo/MoFin")
|
||||
from backtest_framework import prepare_bars as _pb, compute_single_score as _cs
|
||||
from datetime import datetime as _dt2, timedelta as _td2
|
||||
_end2 = _dt2.now().strftime('%Y-%m-%d')
|
||||
_start2 = (_dt2.now() - _td2(days=150)).strftime('%Y-%m-%d')
|
||||
_scored = []
|
||||
for w in weak:
|
||||
_sc = 0
|
||||
try:
|
||||
_bars = _pb(w['code'], _start2, _end2)
|
||||
if _bars and len(_bars) >= 25:
|
||||
_r = _cs(_bars)
|
||||
_sc = _r[0] if _r else 0
|
||||
except Exception:
|
||||
pass
|
||||
_scored.append((_sc, w))
|
||||
_scored.sort(key=lambda x: x[0]) # 评分最低 = 优先套取
|
||||
weak = [w for _, w in _scored]
|
||||
print(" [换仓] 因子评分排序: " + ", ".join(f"{w['name']}({s})" for s, w in _scored[:5]), flush=True)
|
||||
except Exception as _se:
|
||||
print(f" [换仓] 评分排序失败(回退信号排序): {_se}", flush=True)
|
||||
weak = sorted(weak, key=lambda w: ({'弱势持有': 0, '观望': 1}.get(w['timing_signal'], 2),
|
||||
-(w['position_pct'] or 0)))
|
||||
if weak:
|
||||
need_pct = queued[0][1]
|
||||
@@ -1989,7 +2018,7 @@ def push_recommend_alert(conn, code: str):
|
||||
f"区间{el}→{_mid_xmpp}←{eh} 损{sl} 盈{tp} RR={rr or 0} 仓位{pos or '-'}")
|
||||
if fa:
|
||||
msg += f"\n\n【12维完整分析】\n{fa}"
|
||||
return notify("买入信号", msg, ACTION)
|
||||
# 2026-08-26: 通过 messenger_send 完整推送,不走 alert_helper(会截断到30行)
|
||||
except Exception as e:
|
||||
print(f" [ALERT] {code} 推送异常: {e}", flush=True)
|
||||
return False
|
||||
@@ -2305,9 +2334,27 @@ def write_holdings_batch(conn, holdings: list[dict]) -> tuple[bool, str]:
|
||||
return False, f"币种约束: {e}"
|
||||
except sqlite3.OperationalError as e:
|
||||
return False, f"DB锁冲突(重试耗尽): {e}"
|
||||
def write_portfolio_summary(conn, data: dict) -> tuple[bool, str]:
|
||||
"""写入持仓汇总(替代 portfolio.json 顶层)"""
|
||||
def write_portfolio_summary(conn, data: dict, allow_cash_update: bool = False) -> tuple[bool, str]:
|
||||
"""写入持仓汇总(替代 portfolio.json 顶层)
|
||||
|
||||
现金写保护(2026-08-30 老莫立):cash/frozen_cash 只能由「老莫 XMPP 口述 → 知微
|
||||
资产更新流程」修改。默认 allow_cash_update=False:忽略 data 里的 cash/frozen_cash,
|
||||
读库内现值回写并据此重算 total_assets。仅 import_holding_xls(券商导入, --cash
|
||||
必传)等显式通道传 True 才更新现金。8-30 事故:重构期某调用方错写 80476 覆盖了
|
||||
老莫锁定的 690。
|
||||
"""
|
||||
try:
|
||||
if not allow_cash_update:
|
||||
existing = conn.execute(
|
||||
"SELECT cash, frozen_cash FROM portfolio_summary WHERE id=1").fetchone()
|
||||
if existing is not None:
|
||||
data = dict(data)
|
||||
data['cash'] = existing[0]
|
||||
data['frozen_cash'] = existing[1]
|
||||
# 现金被保护时 total_assets 必须按保护后的现金重算,防口径漂移
|
||||
sv = data.get('stock_value') or data.get('total_mv')
|
||||
if sv is not None:
|
||||
data['total_assets'] = sv + data['cash'] + data['frozen_cash']
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
conn.execute("""
|
||||
INSERT INTO portfolio_summary (id, total_assets, total_mv, stock_value,
|
||||
|
||||
@@ -151,3 +151,14 @@ deploy_guard 对 hk_rate 的持续报警同源(仓库已修,生产未更新
|
||||
3. 端到端验证:测试卡 t_a764f411 48 秒 ready→done
|
||||
4. AgentsMeeting 文档同步:learned.md 教训 + kanban-api-reference.md dispatcher 机制节
|
||||
(AgentsMeeting commit `1ad7779` 已 push)
|
||||
|
||||
---
|
||||
|
||||
## 十二、首页资产修复(同日傍晚,老莫报告)
|
||||
|
||||
**问题**:首页总资产不对(1,003,750)+ 无现金卡。
|
||||
**根因**:①portfolio_summary.cash 被重构期某调用方错写 80476(老莫 8-25 锁定 690,cash_log verified);
|
||||
②index.html 顶部资产卡无现金卡(唯一现金渲染在推荐操作区)。
|
||||
**修复**:①cash 修正 690 + cash_log 留痕(dad_stated)+ total_assets 923,964.41;
|
||||
②前端加现金卡(693469e1);③**现金写保护**(write_portfolio_summary 默认 allow_cash_update=False,
|
||||
唯一通道=老莫 XMPP→知微资产更新流程;import_holding_xls --cash 显式放行)——规范进 ops-discipline 第六节。
|
||||
|
||||
@@ -73,3 +73,12 @@ git push origin master:session-work # 或留本地并立即 XMPP 通知小小
|
||||
---
|
||||
|
||||
*依据:dev-spec 十条红线 + 2026-07-20 stale 提交事件 + 2026-07-21 gateway agent 螺旋事件*
|
||||
|
||||
## 六、现金写保护(2026-08-30 老莫立)
|
||||
|
||||
**cash/frozen_cash 唯一合法更新通道:老莫 XMPP 口述 → 知微资产更新流程(manual-asset-update)**。
|
||||
|
||||
- `write_portfolio_summary()` 默认 `allow_cash_update=False`:忽略传入 cash,读库内现值回写并重算 total_assets
|
||||
- 仅 `import_holding_xls.py`(券商导入,--cash 必传)显式放行
|
||||
- 任何现金变更必须先写 cash_log(note 精确来源,如「Dad 在 XMPP 14:05 说现金 xxx」)
|
||||
- 8-30 事故:重构期某调用方错写 80476 覆盖老莫锁定的 690——保护上线后此类覆写不可能再发生
|
||||
|
||||
Reference in New Issue
Block a user