fix: 同步4个库文件副本的提交内容与根canonical一致——消除deploy_guard回滚↔sync重链拉锯(guard实测抓到漂移并回滚,机制自证有效,但提交内容必须同源)
This commit is contained in:
@@ -82,7 +82,7 @@ def read_decisions():
|
|||||||
"created_at, updated_at, "
|
"created_at, updated_at, "
|
||||||
"avg_price, decision_timestamp, note, quality_check, "
|
"avg_price, decision_timestamp, note, quality_check, "
|
||||||
"quality_checked_at, quality_issues_json, position_advice, "
|
"quality_checked_at, quality_issues_json, position_advice, "
|
||||||
"signal_factors_json, time_horizon, decision_type "
|
"signal_factors_json, time_horizon, decision_type, tag "
|
||||||
"FROM holding_strategies WHERE status IN ('active','updated') "
|
"FROM holding_strategies WHERE status IN ('active','updated') "
|
||||||
"ORDER BY code"
|
"ORDER BY code"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|||||||
@@ -262,6 +262,29 @@ def init_all_tables(conn: sqlite3.Connection):
|
|||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_strategy_code ON holding_strategies(code);
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_strategy_code ON holding_strategies(code);
|
||||||
CREATE INDEX IF NOT EXISTS idx_strategy_status ON holding_strategies(status);
|
CREATE INDEX IF NOT EXISTS idx_strategy_status ON holding_strategies(status);
|
||||||
|
|
||||||
|
-- 策略历史快照(每次覆写前自动记录)
|
||||||
|
CREATE TABLE IF NOT EXISTS strategy_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
decision_type TEXT,
|
||||||
|
strategy_type TEXT,
|
||||||
|
full_analysis TEXT,
|
||||||
|
action TEXT,
|
||||||
|
timing_signal TEXT,
|
||||||
|
entry_low REAL,
|
||||||
|
entry_high REAL,
|
||||||
|
stop_loss REAL,
|
||||||
|
take_profit REAL,
|
||||||
|
position_advice TEXT,
|
||||||
|
rr_ratio REAL,
|
||||||
|
version INTEGER,
|
||||||
|
source_trigger TEXT,
|
||||||
|
reassessed_at TEXT,
|
||||||
|
snapshotted_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_strategy_history_code ON strategy_history(code, snapshotted_at);
|
||||||
|
|
||||||
-- 自选股
|
-- 自选股
|
||||||
CREATE TABLE IF NOT EXISTS watchlist_stocks (
|
CREATE TABLE IF NOT EXISTS watchlist_stocks (
|
||||||
code TEXT PRIMARY KEY REFERENCES stocks(code),
|
code TEXT PRIMARY KEY REFERENCES stocks(code),
|
||||||
@@ -551,6 +574,14 @@ def init_all_tables(conn: sqlite3.Connection):
|
|||||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col_def}")
|
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col_def}")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass # column already exists
|
pass # column already exists
|
||||||
|
|
||||||
|
# ── tag 迁移(2026-07-20):推荐标签 current_recommend / active_manual ──
|
||||||
|
# 此前 strategy_lifecycle 在 dict 里设置 tag 但 write_holding_strategy 无此列,
|
||||||
|
# 导致标签在写入时被静默丢弃。补列 + 写入保留。
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE holding_strategies ADD COLUMN tag TEXT DEFAULT ''")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -1077,9 +1108,52 @@ def get_prices_batch_from_db(codes: list[str]) -> dict:
|
|||||||
# 核心写函数 — 替代 json.dump(),强制币种约束
|
# 核心写函数 — 替代 json.dump(),强制币种约束
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool, str]:
|
def snapshot_strategy_history(conn, code: str, source_trigger: str = "write_holding_strategy"):
|
||||||
|
"""在修改前快照当前策略到 strategy_history 表。永不抛异常。"""
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT code, name, decision_type, strategy_type, full_analysis, "
|
||||||
|
"action, timing_signal, entry_low, entry_high, stop_loss, take_profit, "
|
||||||
|
"position_advice, rr_ratio, version, reassessed_at "
|
||||||
|
"FROM holding_strategies WHERE code=? AND status='active'",
|
||||||
|
(code,)
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO strategy_history
|
||||||
|
(code, name, decision_type, strategy_type, full_analysis, action,
|
||||||
|
timing_signal, entry_low, entry_high, stop_loss, take_profit,
|
||||||
|
position_advice, rr_ratio, version, source_trigger, reassessed_at, snapshotted_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
""", (
|
||||||
|
row[0], row[1], row[2], row[3],
|
||||||
|
row[4], row[5], row[6],
|
||||||
|
row[7], row[8], row[9], row[10],
|
||||||
|
row[11], row[12], row[13],
|
||||||
|
source_trigger, row[14], now
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
# 每只股票只保留最近20条历史
|
||||||
|
conn.execute("""
|
||||||
|
DELETE FROM strategy_history WHERE code=? AND id NOT IN (
|
||||||
|
SELECT id FROM strategy_history WHERE code=? ORDER BY snapshotted_at DESC LIMIT 20
|
||||||
|
)
|
||||||
|
""", (code, code))
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [SNAPSHOT] {code} 快照失败: {e}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_holding_strategy(conn, code: str, name: str, data: dict,
|
||||||
|
source_trigger: str = "write_holding_strategy") -> tuple[bool, str]:
|
||||||
"""写入持仓策略(替代 decisions.json 单条写入)。data 必须包含 currency。"""
|
"""写入持仓策略(替代 decisions.json 单条写入)。data 必须包含 currency。"""
|
||||||
try:
|
try:
|
||||||
|
# ── 覆写前快照旧行 ──
|
||||||
|
snapshot_strategy_history(conn, code, source_trigger)
|
||||||
|
|
||||||
|
|
||||||
currency = data.get('currency', 'CNY')
|
currency = data.get('currency', 'CNY')
|
||||||
# Serialize JSON fields
|
# Serialize JSON fields
|
||||||
import json as _json
|
import json as _json
|
||||||
@@ -1091,12 +1165,18 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
|
|||||||
# 在DELETE前保留现有的full_analysis和reassessed_at(防止被regenerate_all等清空)
|
# 在DELETE前保留现有的full_analysis和reassessed_at(防止被regenerate_all等清空)
|
||||||
_existing_fa = data.get('full_analysis', '')
|
_existing_fa = data.get('full_analysis', '')
|
||||||
_existing_ra = data.get('reassessed_at', '')
|
_existing_ra = data.get('reassessed_at', '')
|
||||||
if not _existing_fa:
|
# tag 语义:'tag' 键缺席=保留旧标签;显式传入(含'')= 按传入值(允许清除标签)
|
||||||
|
_tag_absent = 'tag' not in data
|
||||||
|
_existing_tag = data.get('tag', '') or ''
|
||||||
|
if not _existing_fa or _tag_absent:
|
||||||
try:
|
try:
|
||||||
_old = conn.execute("SELECT full_analysis, reassessed_at FROM holding_strategies WHERE code=? ORDER BY id DESC LIMIT 1", (code,)).fetchone()
|
_old = conn.execute("SELECT full_analysis, reassessed_at, tag FROM holding_strategies WHERE code=? ORDER BY id DESC LIMIT 1", (code,)).fetchone()
|
||||||
if _old:
|
if _old:
|
||||||
if _old[0]: _existing_fa = _old[0]
|
if not _existing_fa:
|
||||||
if _old[1]: _existing_ra = _old[1]
|
if _old[0]: _existing_fa = _old[0]
|
||||||
|
if _old[1]: _existing_ra = _old[1]
|
||||||
|
if _tag_absent and _old[2]:
|
||||||
|
_existing_tag = _old[2]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1112,10 +1192,10 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
|
|||||||
avg_price, decision_timestamp, note, quality_check,
|
avg_price, decision_timestamp, note, quality_check,
|
||||||
quality_checked_at, quality_issues_json, position_advice,
|
quality_checked_at, quality_issues_json, position_advice,
|
||||||
signal_factors_json, time_horizon, decision_type,
|
signal_factors_json, time_horizon, decision_type,
|
||||||
full_analysis, reassessed_at)
|
full_analysis, reassessed_at, tag)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,
|
||||||
datetime('now','localtime'),
|
datetime('now','localtime'),
|
||||||
?,?,?,?,?,?,?,?,?,?,?,?)
|
?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
""", (
|
""", (
|
||||||
code, name,
|
code, name,
|
||||||
data.get('version', 1), data.get('price'), data.get('cost'),
|
data.get('version', 1), data.get('price'), data.get('cost'),
|
||||||
@@ -1141,6 +1221,7 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
|
|||||||
# 保留full_analysis和reassessed_at
|
# 保留full_analysis和reassessed_at
|
||||||
_existing_fa,
|
_existing_fa,
|
||||||
_existing_ra,
|
_existing_ra,
|
||||||
|
_existing_tag,
|
||||||
))
|
))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True, f"策略 {code} 已写入"
|
return True, f"策略 {code} 已写入"
|
||||||
|
|||||||
+88
-7
@@ -262,6 +262,29 @@ def init_all_tables(conn: sqlite3.Connection):
|
|||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_strategy_code ON holding_strategies(code);
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_strategy_code ON holding_strategies(code);
|
||||||
CREATE INDEX IF NOT EXISTS idx_strategy_status ON holding_strategies(status);
|
CREATE INDEX IF NOT EXISTS idx_strategy_status ON holding_strategies(status);
|
||||||
|
|
||||||
|
-- 策略历史快照(每次覆写前自动记录)
|
||||||
|
CREATE TABLE IF NOT EXISTS strategy_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
decision_type TEXT,
|
||||||
|
strategy_type TEXT,
|
||||||
|
full_analysis TEXT,
|
||||||
|
action TEXT,
|
||||||
|
timing_signal TEXT,
|
||||||
|
entry_low REAL,
|
||||||
|
entry_high REAL,
|
||||||
|
stop_loss REAL,
|
||||||
|
take_profit REAL,
|
||||||
|
position_advice TEXT,
|
||||||
|
rr_ratio REAL,
|
||||||
|
version INTEGER,
|
||||||
|
source_trigger TEXT,
|
||||||
|
reassessed_at TEXT,
|
||||||
|
snapshotted_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_strategy_history_code ON strategy_history(code, snapshotted_at);
|
||||||
|
|
||||||
-- 自选股
|
-- 自选股
|
||||||
CREATE TABLE IF NOT EXISTS watchlist_stocks (
|
CREATE TABLE IF NOT EXISTS watchlist_stocks (
|
||||||
code TEXT PRIMARY KEY REFERENCES stocks(code),
|
code TEXT PRIMARY KEY REFERENCES stocks(code),
|
||||||
@@ -551,6 +574,14 @@ def init_all_tables(conn: sqlite3.Connection):
|
|||||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col_def}")
|
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col_def}")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass # column already exists
|
pass # column already exists
|
||||||
|
|
||||||
|
# ── tag 迁移(2026-07-20):推荐标签 current_recommend / active_manual ──
|
||||||
|
# 此前 strategy_lifecycle 在 dict 里设置 tag 但 write_holding_strategy 无此列,
|
||||||
|
# 导致标签在写入时被静默丢弃。补列 + 写入保留。
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE holding_strategies ADD COLUMN tag TEXT DEFAULT ''")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -1077,9 +1108,52 @@ def get_prices_batch_from_db(codes: list[str]) -> dict:
|
|||||||
# 核心写函数 — 替代 json.dump(),强制币种约束
|
# 核心写函数 — 替代 json.dump(),强制币种约束
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool, str]:
|
def snapshot_strategy_history(conn, code: str, source_trigger: str = "write_holding_strategy"):
|
||||||
|
"""在修改前快照当前策略到 strategy_history 表。永不抛异常。"""
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT code, name, decision_type, strategy_type, full_analysis, "
|
||||||
|
"action, timing_signal, entry_low, entry_high, stop_loss, take_profit, "
|
||||||
|
"position_advice, rr_ratio, version, reassessed_at "
|
||||||
|
"FROM holding_strategies WHERE code=? AND status='active'",
|
||||||
|
(code,)
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO strategy_history
|
||||||
|
(code, name, decision_type, strategy_type, full_analysis, action,
|
||||||
|
timing_signal, entry_low, entry_high, stop_loss, take_profit,
|
||||||
|
position_advice, rr_ratio, version, source_trigger, reassessed_at, snapshotted_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
""", (
|
||||||
|
row[0], row[1], row[2], row[3],
|
||||||
|
row[4], row[5], row[6],
|
||||||
|
row[7], row[8], row[9], row[10],
|
||||||
|
row[11], row[12], row[13],
|
||||||
|
source_trigger, row[14], now
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
# 每只股票只保留最近20条历史
|
||||||
|
conn.execute("""
|
||||||
|
DELETE FROM strategy_history WHERE code=? AND id NOT IN (
|
||||||
|
SELECT id FROM strategy_history WHERE code=? ORDER BY snapshotted_at DESC LIMIT 20
|
||||||
|
)
|
||||||
|
""", (code, code))
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [SNAPSHOT] {code} 快照失败: {e}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_holding_strategy(conn, code: str, name: str, data: dict,
|
||||||
|
source_trigger: str = "write_holding_strategy") -> tuple[bool, str]:
|
||||||
"""写入持仓策略(替代 decisions.json 单条写入)。data 必须包含 currency。"""
|
"""写入持仓策略(替代 decisions.json 单条写入)。data 必须包含 currency。"""
|
||||||
try:
|
try:
|
||||||
|
# ── 覆写前快照旧行 ──
|
||||||
|
snapshot_strategy_history(conn, code, source_trigger)
|
||||||
|
|
||||||
|
|
||||||
currency = data.get('currency', 'CNY')
|
currency = data.get('currency', 'CNY')
|
||||||
# Serialize JSON fields
|
# Serialize JSON fields
|
||||||
import json as _json
|
import json as _json
|
||||||
@@ -1091,12 +1165,18 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
|
|||||||
# 在DELETE前保留现有的full_analysis和reassessed_at(防止被regenerate_all等清空)
|
# 在DELETE前保留现有的full_analysis和reassessed_at(防止被regenerate_all等清空)
|
||||||
_existing_fa = data.get('full_analysis', '')
|
_existing_fa = data.get('full_analysis', '')
|
||||||
_existing_ra = data.get('reassessed_at', '')
|
_existing_ra = data.get('reassessed_at', '')
|
||||||
if not _existing_fa:
|
# tag 语义:'tag' 键缺席=保留旧标签;显式传入(含'')= 按传入值(允许清除标签)
|
||||||
|
_tag_absent = 'tag' not in data
|
||||||
|
_existing_tag = data.get('tag', '') or ''
|
||||||
|
if not _existing_fa or _tag_absent:
|
||||||
try:
|
try:
|
||||||
_old = conn.execute("SELECT full_analysis, reassessed_at FROM holding_strategies WHERE code=? ORDER BY id DESC LIMIT 1", (code,)).fetchone()
|
_old = conn.execute("SELECT full_analysis, reassessed_at, tag FROM holding_strategies WHERE code=? ORDER BY id DESC LIMIT 1", (code,)).fetchone()
|
||||||
if _old:
|
if _old:
|
||||||
if _old[0]: _existing_fa = _old[0]
|
if not _existing_fa:
|
||||||
if _old[1]: _existing_ra = _old[1]
|
if _old[0]: _existing_fa = _old[0]
|
||||||
|
if _old[1]: _existing_ra = _old[1]
|
||||||
|
if _tag_absent and _old[2]:
|
||||||
|
_existing_tag = _old[2]
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1112,10 +1192,10 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
|
|||||||
avg_price, decision_timestamp, note, quality_check,
|
avg_price, decision_timestamp, note, quality_check,
|
||||||
quality_checked_at, quality_issues_json, position_advice,
|
quality_checked_at, quality_issues_json, position_advice,
|
||||||
signal_factors_json, time_horizon, decision_type,
|
signal_factors_json, time_horizon, decision_type,
|
||||||
full_analysis, reassessed_at)
|
full_analysis, reassessed_at, tag)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,
|
||||||
datetime('now','localtime'),
|
datetime('now','localtime'),
|
||||||
?,?,?,?,?,?,?,?,?,?,?,?)
|
?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
""", (
|
""", (
|
||||||
code, name,
|
code, name,
|
||||||
data.get('version', 1), data.get('price'), data.get('cost'),
|
data.get('version', 1), data.get('price'), data.get('cost'),
|
||||||
@@ -1141,6 +1221,7 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
|
|||||||
# 保留full_analysis和reassessed_at
|
# 保留full_analysis和reassessed_at
|
||||||
_existing_fa,
|
_existing_fa,
|
||||||
_existing_ra,
|
_existing_ra,
|
||||||
|
_existing_tag,
|
||||||
))
|
))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True, f"策略 {code} 已写入"
|
return True, f"策略 {code} 已写入"
|
||||||
|
|||||||
Reference in New Issue
Block a user