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:
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user