Files
auto-stock/backend/alembic/versions/0013_fix_unknown_journal_strategy.py
shuim 45677c8fbb fix: 리스크 결함 일괄 수정 — 누적 4% 상한·스윙 소유권·일일한도 분리·DT 순손익/체결가·시간손절 밴 제외·체결조회 경로
trade_executor
- 종목당 4% 상한을 기존 보유분 포함 누적으로 (sizing_core.remaining_position_room)
- 추천 경로는 스윙 소유 행만 매수·매도 (external/manual/daytrading/us_swing skip, 대칭), 고아 DT 행 warning
- 일일 매수한도는 스윙 소유 매수만 집계, 집계 실패 시 fail-closed
- 부분매도 반복 차단에서만 DT 매도 제외 (재매수 쿨다운은 유지)
- 체결 확인: 부분체결 잔량 자동 재주문 제거(가드 전부 우회·취소/거부 시 발동), PARTIAL→COMPLETED+체결수량, 초과체결 캡

korea_investment_api
- get_order_execution 이 존재하지 않는 /trading/inquire-ccnl 을 호출하던 결함 → inquire-daily-ccld
  (TTTC0081R 우선, 구 TR 폴백, 페이지네이션, ODNO 매칭, EXCG_ID_DVSN_CD=KRX), 실패 로그 warning

daytrading
- 시간손절은 loss_tracker 중립 (42일 밴이 스윙까지 막던 문제)
- 실현손익을 수수료·거래세 차감 순손익으로 (trade_cost_core), 체결가 백그라운드 보정 (fill_reconciler)
- legacy _record_trade strategy_type=daytrading 명시

db_worker
- update_trade_history_by_order_id 를 당일(KST)+최신 1행으로 한정 (일별 재사용 ODNO 로 과거 행 덮어쓰기 방지)

alembic 0013: trade_journals strategy_name='unknown' 을 trade_history.strategy_type 으로 보정 (백업 테이블 포함)
CLAUDE.md: 변경 동작 반영, MT 예약 리비전 0013→0014

998 passed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 17:37:29 +09:00

147 lines
5.0 KiB
Python

"""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}건 (백업 테이블은 유지)")