Skip to content

fix: 적립금이 매수로 전환되지 못하던 다섯 개 차단 해소 - #457

Merged
easygap merged 1 commit into
mainfrom
fix/pocket-contribution-blockers
Aug 26, 2026
Merged

fix: 적립금이 매수로 전환되지 못하던 다섯 개 차단 해소#457
easygap merged 1 commit into
mainfrom
fix/pocket-contribution-blockers

Conversation

@easygap

@easygap easygap commented Aug 26, 2026

Copy link
Copy Markdown
Owner

배경

kr_pocket 첫 적립금 10만원을 기록하고, 다음 사이클이 그 현금을 어떻게 흡수하는지 운영 DB 사본에서 미리 돌려 봤다. 매수가 한 건도 안 나갔다.

개별 주식용으로 만든 전역 파라미터가 ETF 2종 바스켓의 설계를 차례로 막고 있었다. 하나를 풀면 다음 게 걸리는 식으로 다섯 개였다.

전역값 kr_pocket 선언 거부 사유
max_position_ratio 20% 종목당 47.5% 단일 종목 비중 20% 초과
max_investment_ratio 70% 투자 95% (위를 통과해도 곧 걸림)
min_cash_ratio 20% 현금 5% 최소 현금 비중 20% 미만
max_sector_ratio + sector_map_strict 069500 업종 매핑 없음
skip_earnings_days 3 실적일 조회 불가

다섯 개 다 fail-closed라 증상은 '매수 0건'과 '배치율 미달'로만 나타났다. 원인은 어디에도 안 보였다. kr_pocket이 7/10 이후 한 주도 못 산 게 이것 때문이다.

접근

전역값은 재량 매매용 안전판이다. 목표 비중을 명시로 선언한 주문에는 그 선언에서 파생한 상한으로 바꿔 단다. 없애는 게 아니라 위임이다.

  • 어떤 경우에도 목표 + 드리프트 임계값을 넘길 수 없다
  • 위임은 weight_policy_managed 주문에만 적용된다. 비중표에 없는 종목은 전역 상한을 그대로 쓴다
  • 면제 대상이 아닌 검사(노출 상한·유동성·갭·현금·거래중단)는 전부 그대로다

같은 성격의 위임을 이미 두 번 했다. 쌍별 상관 거부권(#456)과 단타 손절 기준이 그것이고, 이번이 세 번째다. docs/OPERATING_PRINCIPLES.md 원칙 3에 적어 둔 패턴이 실제로 또 나왔다.

1. 노출 상한 위임

리밸런서가 자기 비중표에서 계산해 executor로 넘긴다.

max_position_ratio   = (목표비중 + 드리프트 임계값) × 주식비중
max_investment_ratio = 주식비중 + 배치 밴드
min_cash_ratio       = 바스켓이 선언한 값 (effective_stock_fraction과 같은 해석)

kr_diversified_hold로 검산하면 종목 상한이 (11.1% + 8%) × 60% = 11.5%로 전역 20%보다 더 조인다. 위임이 곧 완화는 아니다.

2. 업종·실적 필터 — core/instrument_classes.py 신설

이 둘은 개별 기업으로만 의미가 있는 검사다. 지수 ETF는 그 자체가 여러 업종에 걸친 묶음이고, 금리 파킹 ETF는 주식도 아니다. KRX 업종 코드도 실적 발표일도 DART 기업코드도 없다.

면제 목록을 업종 쪽과 실적 쪽에 따로 만들면 갈라지므로 instrument_classes.non_company_symbols 한 곳에 두고 둘 다 참조하게 했다.

개별 주식의 업종 매핑 누락은 계속 fail-closed다. 테스트로 고정해 뒀다.

검증

운영 DB 사본에 실제 실행 경로를 태웠다.

kr_pocket            배치율 58.0% → 73.1%   357870 1주 체결
kr_diversified_hold  배치율 54.9% → 59.8%   012330 1주 체결 (회귀 없음)
입금 후 TWR -4.93%   입금이 수익으로 잡히지 않는다

CI 환경 재현 — config/settings.yaml을 치우고 전체 스위트 실행: 1966개 통과.

남은 것

  • kr_pocket이 설계 배치율 95%에 닿으려면 적립이 몇 달 더 쌓여야 한다. 069500 1주가 약 10.8만원이라 잔고 38만원에서는 한 번에 채울 수 없다. 정상 수렴 과정이다.
  • _execute_buy_quantity_impl의 1회 손실 예산이 아직 전역 -3% 손절로 계산된다. 트랙 정책을 넣으면 슬롯이 예산을 넘어 재동결되므로 목표비중 주문에는 별도 기준이 필요하다(#456에서 이미 기록해 둔 항목).

kr_pocket에 첫 적립금 10만원을 기록하고 다음 사이클을 사본에서 돌려 봤더니
매수가 한 건도 나가지 않았다. 개별 주식용 전역 파라미터가 ETF 2종 바스켓의
설계를 차례로 막고 있었다. 하나를 풀면 다음 게 걸리는 식으로 다섯 개였다.

  단일 종목 20% 상한  vs 선언 47.5%  → '단일 종목 비중 20% 초과'
  전체 투자 70% 상한  vs 선언 95%
  최소 현금 20%       vs 선언 5%     → '최소 현금 비중 20% 미만'
  업종 비중(KRX)      ETF는 업종 코드 없음  → '업종 매핑 없음'
  실적 발표일 필터    ETF는 실적일 없음     → '실적일 조회 불가'

다섯 개 다 fail-closed라 증상은 '매수 0건'과 '배치율 미달'로만 나타났다. 원인은
어디에도 안 보였다. kr_pocket이 7/10 이후 한 주도 못 산 이유가 이것이다.

전역값은 재량 매매용 안전판이다. 목표 비중을 명시로 선언한 주문에는 그 선언에서
파생한 상한으로 바꿔 단다. 없애는 게 아니다 — 어떤 경우에도 목표 + 드리프트
임계값을 넘길 수 없고, 위임은 weight_policy_managed 주문에만 적용된다.

노출 상한 (max_position / max_investment / min_cash):
리밸런서가 자기 비중표에서 계산해 넘긴다. 종목 상한은 (목표 + 드리프트) × 주식비중,
투자 상한은 주식비중 + 배치 밴드, 현금 하한은 바스켓이 선언한 값
(effective_stock_fraction과 같은 해석 규칙). 비중표에 없는 종목은 위임하지 않는다.

업종·실적 필터 (core/instrument_classes.py 신설):
이 둘은 '개별 기업'으로만 의미가 있는 검사다. 지수 ETF는 그 자체가 여러 업종에
걸친 묶음이고, 금리 파킹 ETF는 주식도 아니다. 종목별 면제 목록을 두 곳에 따로
만들면 갈라지므로 instrument_classes.non_company_symbols 한 곳에 두고 둘 다
참조한다. 면제는 이 두 검사뿐 — 노출 상한·유동성·갭·현금·거래중단은 그대로다.
개별 주식의 업종 매핑 누락은 계속 fail-closed다(테스트로 고정).

검증(운영 DB 사본, 실제 실행 경로):
  kr_pocket            배치율 58.0% → 73.1%, 357870 1주 체결
  kr_diversified_hold  배치율 54.9% → 59.8%, 012330 1주 체결 (회귀 없음)
  입금 후 TWR -4.93% — 입금이 수익으로 잡히지 않는다
CI 환경 재현(config/settings.yaml 치우고 실행): 1966개 통과.
@easygap
easygap merged commit 32320a8 into main Aug 26, 2026
1 check passed
@easygap
easygap deleted the fix/pocket-contribution-blockers branch August 26, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant