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