"""backfill trade_journals.strategy_name='unknown' from trade_history.strategy_type (data-only) Revision ID: 0013 Revises: 0012 Create Date: 2026-10-01 [배경] 매매일지 생성 경로가 전략명을 판정하지 못해 `trade_journals.strategy_name` 이 'unknown' 으로 저장된 행(약 187건)이 있다. 코드는 이미 수정됐으나 기존 행은 그대로다. `trade_history.strategy_type` 이 신뢰 가능한 원본이므로 그 값으로 보정한다. [매핑] trade_analyzer._parse_strategy(strategy_type 분기)와 동일하다. swing -> mean_reversion / daytrading -> day_trade / kosdaq_scan -> kosdaq_scanner short_term -> short_term / 그 외 -> lower(trim(strategy_type)) 그대로 tags 는 [strategy, exit_type, ...] 형태(db_worker/trade_analyzer)이므로 첫 요소가 'unknown' 일 때만 같은 전략명으로 치환한다. 배열이 아니거나 비어 있으면 그대로 둔다. [안전] - 백업 테이블 `trade_journals_bak_strategy_20261001` 에 대상 행의 (id, strategy_name, tags) 를 복사한다(IF NOT EXISTS + 이미 있는 id 는 재삽입 안 함 -> 재실행 안전). - 테이블/컬럼이 없으면(신규 볼륨, 앱 기동 전) 조용히 no-op (0012 와 동일한 가드). - downgrade 는 백업에서 strategy_name/tags 복원. 백업 테이블은 남긴다. 스키마 변경은 백업 테이블 생성뿐이다. """ from alembic import op import sqlalchemy as sa revision = "0013" down_revision = "0012" branch_labels = None depends_on = None _BAK = "trade_journals_bak_strategy_20261001" # trade_analyzer._parse_strategy 의 strategy_type 매핑과 동일해야 한다. _MAPPED = """ CASE lower(btrim(th.strategy_type)) WHEN 'swing' THEN 'mean_reversion' WHEN 'daytrading' THEN 'day_trade' WHEN 'kosdaq_scan' THEN 'kosdaq_scanner' WHEN 'short_term' THEN 'short_term' ELSE lower(btrim(th.strategy_type)) END """ # 대상 조건 (백업/갱신 공통) _TARGET_FROM = """ FROM trade_journals j JOIN trade_history th ON th.id = j.trade_id WHERE j.strategy_name = 'unknown' AND j.trade_id > 0 AND th.strategy_type IS NOT NULL AND btrim(th.strategy_type) <> '' """ _CREATE_BAK = sa.text( f"CREATE TABLE IF NOT EXISTS {_BAK} (id integer PRIMARY KEY, strategy_name varchar(100), tags jsonb)" ) _INSERT_BAK = sa.text( f""" INSERT INTO {_BAK} (id, strategy_name, tags) SELECT j.id, j.strategy_name, j.tags {_TARGET_FROM} AND NOT EXISTS (SELECT 1 FROM {_BAK} b WHERE b.id = j.id) """ ) _UPDATE = sa.text( f""" UPDATE trade_journals j SET strategy_name = {_MAPPED}, tags = CASE WHEN jsonb_typeof(j.tags) = 'array' THEN CASE WHEN jsonb_array_length(j.tags) > 0 AND j.tags -> 0 = to_jsonb('unknown'::text) THEN jsonb_set(j.tags, '{{0}}', to_jsonb({_MAPPED})) ELSE j.tags END ELSE j.tags END, updated_at = now() AT TIME ZONE 'Asia/Seoul' FROM trade_history th WHERE th.id = j.trade_id AND j.strategy_name = 'unknown' AND j.trade_id > 0 AND th.strategy_type IS NOT NULL AND btrim(th.strategy_type) <> '' """ ) _RESTORE = sa.text( f""" UPDATE trade_journals j SET strategy_name = b.strategy_name, tags = b.tags, updated_at = now() AT TIME ZONE 'Asia/Seoul' FROM {_BAK} b WHERE b.id = j.id """ ) def _has_table(conn, table: str) -> bool: return conn.execute(sa.text( "SELECT 1 FROM information_schema.tables WHERE table_name=:t" ), {"t": table}).fetchone() is not None def _has_columns(conn, table: str, cols) -> bool: rows = conn.execute(sa.text( "SELECT column_name FROM information_schema.columns WHERE table_name=:t" ), {"t": table}).fetchall() have = {r[0] for r in rows} return set(cols) <= have def _schema_ready(conn) -> bool: return ( _has_table(conn, "trade_journals") and _has_table(conn, "trade_history") and _has_columns(conn, "trade_journals", ["id", "trade_id", "strategy_name", "tags", "updated_at"]) and _has_columns(conn, "trade_history", ["id", "strategy_type"]) ) def upgrade() -> None: conn = op.get_bind() if not _schema_ready(conn): print("[0013] trade_journals/trade_history 없음 — 보정 스킵") return conn.execute(_CREATE_BAK) bak = conn.execute(_INSERT_BAK).rowcount upd = conn.execute(_UPDATE).rowcount print(f"[0013] unknown 일지 보정: 백업 {bak}건 추가, 갱신 {upd}건") def downgrade() -> None: conn = op.get_bind() if not _has_table(conn, _BAK) or not _has_table(conn, "trade_journals"): print("[0013] 백업 테이블 없음 — 복원 스킵") return n = conn.execute(_RESTORE).rowcount print(f"[0013] 일지 strategy_name/tags 복원: {n}건 (백업 테이블은 유지)")