fix(audit): candidates UPSERT保计算列+market_scanner INSERT对齐表结构+拆除sync_decisions_to_db地雷

举一反三审计结果:
- accumulation_scanner: INSERT OR REPLACE整行替换,promoted=1的候选被重置score/promoted/log
  → ON CONFLICT DO UPDATE只更新扫描器自有列(cron活跃,今日必修)
- market_scanner: INSERT引用表中不存在的列(price/score/entry_low等),实测必失败
  → 对齐真实schema+同款UPSERT(孤儿脚本顺手修)
- sync_decisions_to_db.py: 全表DELETE再从已不存在的decisions.json重建,地雷→归档
This commit is contained in:
hmo
2026-07-23 14:23:06 +08:00
parent 12f7f87a2e
commit 6eab2148eb
3 changed files with 20 additions and 10 deletions
@@ -227,11 +227,16 @@ def main():
).fetchone()
if exists:
continue
# UPSERT:只更新扫描器自有列,保留 score_*/pass_*/promoted/log/zhiwei_* 等计算列
# (原 INSERT OR REPLACE 会把 promoted=1 的行整行替换,计算列全部清零——2026-07-23 审计发现)
conn.execute(
"INSERT OR REPLACE INTO candidates (code, name, sector, reason, "
"INSERT INTO candidates (code, name, sector, reason, "
"entry_range, stop_loss, target, created_at) "
"VALUES (?,?,?,?,?,?,?,datetime('now','localtime'))",
"VALUES (?,?,?,?,?,?,?,datetime('now','localtime')) "
"ON CONFLICT(code) DO UPDATE SET "
"name=excluded.name, sector=excluded.sector, reason=excluded.reason, "
"entry_range=excluded.entry_range, stop_loss=excluded.stop_loss, target=excluded.target",
(code, name, "accumulation",
f"主力建仓特征({reasons}) 评分{score}/7",
f"{entry_low}~{entry_high}", sl, tp)
+12 -7
View File
@@ -159,17 +159,22 @@ def scan_candidates():
new_candidates.append(candidate)
print(f"{code} {name}{price} (+{chg:.1f}%) 评分{score}", flush=True)
# 4. 写入candidates表
# 4. 写入candidates表(对齐真实表结构:表无 price/change_pct/score/entry_low/entry_high/
# take_profit/source 列,原 INSERT 必失败——2026-07-23 审计实测确认。
# UPSERT 只更新扫描器自有列,保留 score_*/pass_*/promoted/log 等计算列)
if new_candidates:
conn = get_conn()
for c in new_candidates:
conn.execute(
"INSERT OR REPLACE INTO candidates (code, name, price, change_pct, score, "
"entry_low, entry_high, stop_loss, take_profit, source, sector, reason, created_at) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now','localtime'))",
(c["code"], c["name"], c["price"], c["change_pct"], c["score"],
c["entry_low"], c["entry_high"], c["stop_loss"], c["take_profit"],
c["source"], c["sector"], c["reason"])
"INSERT INTO candidates (code, name, sector, reason, "
"entry_range, stop_loss, target, created_at) "
"VALUES (?,?,?,?,?,?,?,datetime('now','localtime')) "
"ON CONFLICT(code) DO UPDATE SET "
"name=excluded.name, sector=excluded.sector, reason=excluded.reason, "
"entry_range=excluded.entry_range, stop_loss=excluded.stop_loss, target=excluded.target",
(c["code"], c["name"], c.get("sector") or c.get("source", "market_scanner"),
f"{c['reason']}{c['price']}(+{c['change_pct']:.1f}%) 评分{c['score']}",
f"{c['entry_low']}~{c['entry_high']}", c["stop_loss"], c["take_profit"])
)
conn.commit()
conn.close()
@@ -1,92 +0,0 @@
#!/usr/bin/env python3
"""将 decisions.json 全量同步到 SQLite holding_strategies 表"""
import json, sqlite3, sys
DECISIONS_PATH = '/home/hmo/web-dashboard/data/decisions.json'
DB_PATH = '/home/hmo/web-dashboard/data/mofin.db'
def main():
# 读 decisions.json
with open(DECISIONS_PATH) as f:
data = json.load(f)
entries = data.get('decisions', [])
print(f'Read {len(entries)} entries from decisions.json')
db = sqlite3.connect(DB_PATH)
# 先清空 holding_strategies(全量重建更干净)
db.execute('DELETE FROM holding_strategies')
inserted = 0
for d in entries:
code = d.get('code')
if not code:
continue
# 从 decisions.json 提取字段,映射到 DB schema
sql = '''INSERT INTO holding_strategies (
code, name, version, price, cost, shares,
stop_loss, take_profit, entry_low, entry_high,
currency, strategy_type, action, timing_signal,
rr_ratio, tech_snapshot, stock_category, sector_context,
status, trigger_json, changelog_json, source, reason,
created_at, updated_at,
avg_price, decision_timestamp, note, decision_type
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'''
# 确定 type/strategy_type
stype = d.get('strategy_type') or d.get('type') or '持仓策略'
# decision_type = d.get('decision_type') or stype
decision_type = stype
vals = (
code,
d.get('name', ''),
d.get('version', 1),
d.get('price'),
d.get('cost'),
d.get('shares', 0),
d.get('stop_loss'),
d.get('take_profit'),
d.get('entry_low'),
d.get('entry_high'),
d.get('currency', 'CNY' if code.startswith(('6','0','3','5')) else 'HKD'),
stype,
d.get('action', ''),
d.get('timing_signal', ''),
d.get('rr_ratio'),
d.get('tech_snapshot'),
d.get('stock_category'),
d.get('sector_context'),
d.get('status', 'active'),
json.dumps(d.get('trigger', {}), ensure_ascii=False) if d.get('trigger') else d.get('trigger_json'),
json.dumps(d.get('changelog', []), ensure_ascii=False) if d.get('changelog') else d.get('changelog_json'),
d.get('source', 'auto'),
d.get('reason'),
d.get('created_at'),
d.get('updated_at'),
d.get('avg_price'),
d.get('decision_timestamp') or d.get('timestamp'),
d.get('note'),
decision_type,
)
try:
db.execute(sql, vals)
inserted += 1
except Exception as e:
print(f'Error inserting {code} ({d.get("name")}): {e}')
db.commit()
# 验证
cnt = db.execute('SELECT COUNT(*) FROM holding_strategies').fetchone()[0]
active = db.execute('SELECT COUNT(*) FROM holding_strategies WHERE status IN ("active","updated")').fetchone()[0]
db.close()
print(f'Synced: {inserted} rows inserted')
print(f'holding_strategies: {cnt} total, {active} active/updated')
return 0 if inserted > 0 else 1
if __name__ == '__main__':
sys.exit(main())