refactor: retire price_events.json completely — DB is the only store

User directive: no JSON, retire it fully, fix all related code.

Changes:
- price_monitor.py: record_event writes DB only; removed EVENTS_PATH/
  load_events/save_events entirely
- strategy_feedback.py: price events read from DB only (removed JSON fallback)
- system_health_check.py: removed price_events.json from file-check list,
  DB-only event stats (was showing 0/0 due to wrong-DB resolution)
- mo_config.py: removed dead price_events_path property (no callers)
- mofin_health.py: price_events freshness reads DB table (authoritative now)
- mofin_db.py: DATA_DIR/DB_PATH now ABSOLUTE (/home/hmo/MoFin/data) —
  was relative __file__.parent, so each hardlinked copy of mofin_db.py
  resolved to a DIFFERENT database (canonical vs web-dashboard vs
  profile-local third DB with 0 rows of everything except market_snapshots).
  This fragmentation was the real cause of health checks reading empty tables.
- Unified all 4 mofin_db copies (root/scripts/deploy/profile) via hardlink
- price_events.json archived to trashbox (fully backfilled: 6353 rows in DB)

Verified:
- record_event lands in DB only, JSON not recreated
- system_health_check: 历史事件 6353 / 今日事件 2965 (was 0/0)
- strategy_feedback + price_monitor full runs clean
This commit is contained in:
hmo
2026-07-20 18:10:05 +08:00
parent efdfaf956a
commit d5b8bec897
16 changed files with 388 additions and 203 deletions
+24
View File
@@ -0,0 +1,24 @@
import sqlite3
for label, path in [('canonical', '/home/hmo/MoFin/data/mofin.db'),
('profile-local', '/home/hmo/.hermes/profiles/position-analyst/scripts/data/mofin.db'),
('web-dashboard', '/home/hmo/web-dashboard/data/mofin.db')]:
try:
c = sqlite3.connect(path, timeout=5)
tables = [r[0] for r in c.execute("SELECT name FROM sqlite_master WHERE type='table'")]
print(f'=== {label}: {path}')
for t in ['price_events', 'holdings', 'live_prices', 'holding_strategies', 'market_snapshots']:
if t in tables:
cnt = c.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
try:
col = 'created_at' if t in ('price_events', 'market_snapshots') else 'updated_at'
last = c.execute(f"SELECT MAX({col}) FROM {t}").fetchone()[0]
except Exception:
last = '?'
print(f' {t}: {cnt} rows, last={last}')
else:
print(f' {t}: MISSING')
c.close()
except Exception as e:
print(f'{label}: ERROR {e}')
print()
-5
View File
@@ -65,11 +65,6 @@ class MoConfig:
warnings.warn("watchlist_path is deprecated — use mo_data.read_watchlist() for DB data", DeprecationWarning, stacklevel=2)
return Path()
@property
def price_events_path(self) -> Path:
"""⚠️ 已废弃!数据在 mofin.db price_events 表。"""
return self.data_dir / "price_events.json"
@property
def live_prices_path(self) -> Path:
"""⚠️ DEPRECATED: 实时价格已迁移到 mofin_db.live_prices 表。"""
+1 -1
View File
@@ -21,7 +21,7 @@ from datetime import datetime
from pathlib import Path
from typing import Optional, Callable
DATA_DIR = Path(__file__).parent / "data"
DATA_DIR = Path("/home/hmo/MoFin/data") # 绝对路径:全系统唯一权威数据目录
DB_PATH = DATA_DIR / "mofin.db"
# ═══════════════════════════════════════════════════════════
+25
View File
@@ -0,0 +1,25 @@
import json, urllib.request
msg = """[笑笑] 补充:price_events 已统一到 DB
你问得对——"不是都全 DB 了吗",之前确实不一致:
【查出的问题】
- price_monitor 的 record_event() 只写 price_events.json,从没调过 mofin_db 的写入函数
- 更隐蔽的是:price_events.code 有外键 → stocks(code),未注册的股票(新候选、港股)写 DB 时外键失败被静默吞掉,这就是 7/6 之后 DB 表再也没新事件的真正原因
【已修复】
1. record_event 改为双写:DB(权威)+ JSON(兼容 mo_config/strategy_feedback 等遗留读取方)
2. 写事件前自动把未注册股票登记进 stocks 表,外键不再失败
3. 一次性回填:JSON 里 4064 条历史事件全部灌入 DB(现共 6353 行,最新到今天 17:06)
4. 实测:新事件 DB/JSON 双落盘 ✅
以后价格事件的权威存储就是 DB 表,健康监控的 db_freshness 也是读它了。"""
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
req = urllib.request.Request("http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"})
try:
print("XMPP:", urllib.request.urlopen(req, timeout=10).read().decode()[:80])
except Exception as e:
print("XMPP fail:", e)
+21 -31
View File
@@ -12,7 +12,6 @@ from mo_data import read_decisions
BREACH_PATH = "/home/hmo/.hermes/zone_breach.json"
STATE_PATH = "/home/hmo/.hermes/price_trigger_state.json"
EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json"
# DB 模块(同步实时价到 mofin.db)
sys.path.insert(0, "/home/hmo/MoFin")
@@ -319,37 +318,28 @@ def save_breaches(data):
json.dump(data, f, ensure_ascii=False, indent=2)
def load_events():
try:
with open(EVENTS_PATH) as f:
return json.load(f)
except:
return {"events": []}
def save_events(events):
os.makedirs(os.path.dirname(EVENTS_PATH), exist_ok=True)
with open(EVENTS_PATH, 'w') as f:
json.dump(events, f, ensure_ascii=False, indent=2)
def record_event(code, name, event_type, price, trigger_value, event_label=""):
"""记录一次价格触发事件到 price_events.json"""
events = load_events()
"""记录一次价格触发事件到 DB price_events 表(唯一权威存储,JSON 已退役)。
price_events.code 有 FK -> stocks(code),未注册的股票(新候选/港股)
先注册再写事件,否则 FK 失败事件丢失。
"""
now = datetime.now().isoformat()
events["events"].append({
"code": code,
"name": name,
"event_type": event_type, # entry_zone, stop_loss, take_profit, exit_zone
"price": round(price, 2),
"trigger_value": trigger_value,
"event_label": event_label,
"timestamp": now,
"date": datetime.now().strftime("%Y-%m-%d"),
})
# 保留最近10000条
events["events"] = events["events"][-10000:]
save_events(events)
if HAS_DB:
try:
from mofin_db import get_conn, write_price_event
_c = get_conn()
_exch, _typ = ("HK", "H") if len(str(code)) == 5 else (("SH", "A") if str(code).startswith(("6", "9")) else ("SZ", "A"))
_c.execute("INSERT OR IGNORE INTO stocks (code, name, exchange, type, updated_at) VALUES (?,?,?,?,?)",
(str(code), name or str(code), _exch, _typ, now))
_c.commit()
write_price_event(_c, code=code, name=name, event_type=event_type,
price=round(price, 2), trigger_value=str(trigger_value),
event_label=event_label)
_c.close()
except Exception as e:
print(f"[price_events DB写入失败] {e}", file=sys.stderr)
def get_trigger_zones(trigger):
@@ -581,7 +571,7 @@ def run_once(round_label=""):
# === 第三步:买入区偏离检测 + 自动重评 ===
reassesed_codes = []
# 先做急跌检测(仅持仓,自选股不推送暴跌告警)
holdings_codes = {d["code"] for d in active if d.get("shares", 0) > 0}
holdings_codes = {d["code"] for d in active if (d.get("shares") or 0) > 0}
for d in active:
code = d["code"]
# 非持仓跳过
+12
View File
@@ -0,0 +1,12 @@
import ast, sys
files = ['price_monitor', 'strategy_feedback', 'system_health_check', 'mo_config', 'mofin_health']
ok = True
for f in files:
p = f'/home/hmo/MoFin/deploy/profile-scripts/{f}.py'
try:
ast.parse(open(p).read())
print('OK', f)
except SyntaxError as e:
print('FAIL', f, e)
ok = False
sys.exit(0 if ok else 1)
+16
View File
@@ -0,0 +1,16 @@
import sys, os
sys.path.insert(0, '/home/hmo/.hermes/profiles/position-analyst/scripts')
import price_monitor as pm
pm.record_event('000850', '华茂股份', 'entry_zone', 3.98, '3.91~4.04', '加仓区间')
print('record_event done')
import sqlite3
c = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
r = c.execute("SELECT code,name,price,created_at FROM price_events WHERE code='000850' ORDER BY id DESC LIMIT 1").fetchone()
print('DB:', r)
c.execute("DELETE FROM price_events WHERE code='000850' AND created_at > '2026-07-20 17:50'")
c.commit()
print('cleaned test row')
# JSON must NOT be recreated
print('JSON exists?', os.path.exists('/home/hmo/web-dashboard/data/price_events.json'))