Skip to content

대형주 손절·낙폭 한도와 적립 계좌 주문 계획 수정, 투자 방식 조사 추가 - #474

Merged
easygap merged 7 commits into
mainfrom
feat/market-research-improvements-20261020
Oct 20, 2026
Merged

easygap merged 7 commits into
mainfrom
feat/market-research-improvements-20261020

Conversation

@easygap

@easygap easygap commented Oct 20, 2026

Copy link
Copy Markdown
Owner

10월 급락 때 대형주 계좌의 손절이 시장 전체 하락에 연달아 걸렸습니다. 9/21과 10/15에 현대모비스·현대차·삼성전자를 평균 매입가 −25%에서 팔았는데, 그사이 KODEX 200도 30% 가까이 빠져 있었습니다. 판 돈은 계좌 낙폭 15% 한도에 막혀 다시 투자되지 못했고, 주식 비중이 설계 60%에서 45%까지 내려갔습니다. ETF 적립 계좌는 주식 비중이 목표보다 20%p 넘게 낮은데도 1주 단위 때문에 주문이 나오지 않았습니다.

개인 투자자의 거래 실태와 많이 쓰는 자동매매 도구, 공개된 뒤에도 성과가 남은 운용 방식, 지금 시장 상황도 함께 조사했습니다. 새 매매 신호를 더할 근거는 없었고, 이미 정해 둔 규칙이 설계대로 돌도록 고쳤습니다.

바뀐 동작

  • 대형주 손절 기준: 평균 매입가가 아니라 같은 보유 기간의 KODEX 200보다 25% 넘게 뒤처질 때 팝니다. 시장 전체가 빠질 때는 걸리지 않고, 한 종목만 크게 무너질 때 걸립니다. 지금 시장 가격을 확인하지 못한 날은 판단을 미룹니다.
  • 계좌 낙폭 한도: 대형주 계좌는 낙폭이 15%를 넘어도 정해 둔 주식 비중(60% + 3%p)까지는 다시 삽니다. 비중 상한은 주문 단계에서 그대로 검사하고, 평가 화면의 매수 가능 상태도 같은 기준으로 표시합니다.
  • 적립 계좌 주문 계획: 종목이 5개 이하인 바스켓은 종목마다 따로 내림하지 않고 1주 단위 수량 조합을 함께 고릅니다. CD ETF와 현금 사이의 차이는 따지지 않아 비용만 드는 매도는 하지 않고, 적립금이 들어오면 모자란 자산부터 삽니다.
  • 주문 직전 시세 확인: 시세 날짜가 오늘이 아니거나, 가격이 전일 종가보다 30% 넘게 벗어나 있으면 주문하지 않습니다.
  • 넣은 돈 기준 수익률: 평가와 주간 보고에 시간가중 수익률과 함께 금액가중 수익률을 보여 줍니다.
  • KIS 주문 규격: 현금 주문을 공식 예제의 새 거래코드(TTTC0012U·0011U)와 거래소 구분 코드로 바꿨습니다.
  • 테스트: 실행 날짜·시각이나 앞선 테스트가 남긴 환경에 따라 실패하던 테스트 4개를 고쳤습니다.

확인한 결과

  • 같은 9종목으로 다시 계산해 보니 지금까지 쓰던 조합(평균 매입가 손절 + 낙폭 한도)이 2014-07 이후와 2021-12 이후 두 구간 모두 가장 나빴습니다(연 4.12%·3.46%, 손절 없음은 5.58%·5.99%). 시장 대비 손절은 손절 없음과 수익률이 비슷해서, 수익을 올리는 장치가 아니라 한 종목 사고를 막는 장치로 남겼습니다.
  • 운영 DB를 읽기 전용으로 복사해 10/20 종가로 실제 계획→실행 경로를 돌렸습니다. 대형주 계좌는 낙폭 18%대에서도 보충 매수 2건이 체결돼 주식 비중이 44.6%에서 50.4%가 됐고, 손절한 종목은 다시 사지 않았습니다. 적립 계좌는 추세가 회복되면 CD ETF 2주를 팔아 KODEX 200 1주를 사는 계획이 나옵니다. 실행 전후 원본 DB 해시는 같았습니다.
  • 적립 계좌 계획기를 2020-07~2026-10 과거 자료로 다시 돌려 보니 연 12.53% → 12.56%, 최대 낙폭 −15.82% → −15.44%로 거의 같았습니다. 비용을 3배로 잡아도 결과는 같았습니다.
  • 로컬 전체 테스트 2,709개가 모두 통과했습니다. 작업 전 main에서는 3개가 늘 실패하고 1개가 가끔 실패했습니다.

운영 시 확인할 점

지금 들고 있는 대형주 6종목은 시장 대비 +7.7~+46.7%라 이번 변경으로 바로 팔리는 종목은 없습니다. 손절한 3종목은 60일 재매수 제한이 풀리는 11/20(현대모비스), 12/14(삼성전자·현대차)까지 사지 않으므로, 그때까지는 주식 비중이 60%를 다 채우지 못합니다. 10월은 실적 발표가 몰려 있어 실적일 필터 때문에 일부 매수가 며칠 미뤄질 수 있습니다.

KIS 새 주문 규격은 요청 구성만 테스트로 확인했습니다. 실전으로 넘어가기 전에 모의 서버에서 1주 매수·매도를 한 번 해 봐야 합니다. 9·10월 적립금 기록, DART 키 재발급, 이메일 알림 비밀번호는 아직 남아 있습니다.

다자산 편입, KODEX 200TR 교체, 대형주 계좌 현금의 CD 운용, 같은 주식 비중의 비교지수는 이번에 넣지 않았습니다. 이유는 조사 문서에 정리했습니다.

main에서 전체 테스트를 돌리면 3건이 늘 실패했고, 1건은 가끔 실패했다.
코드 문제가 아니라 테스트가 실행 날짜와 시각, 앞선 테스트가 남긴 환경에 따라
결과가 달라지는 문제였다.

- test_audit_cycle: 시계를 10/13 10:10으로 고정해 두고 기대값은 실제 오늘
  날짜로 계산하고 있었다. 10/13 하루만 통과했다. 고정한 날짜로 비교한다.
- test_critical_fixes: 정규장 검사가 들어간 뒤로 15:30 이후에 돌리면 매매
  경로를 타지 않아 실패했다. 정규장 여부도 고정한다.
- test_config_auto_entry: 원래 없던 환경변수를 delenv(raising=False)로 지우면
  monkeypatch가 아무것도 기록하지 않는다. 그래서 테스트가 넣은 디스코드 주소가
  끝난 뒤에도 남아, 뒤에 도는 알림 테스트가 discord.test로 발송을 시도했다.
- test_audit_runtime_post_market: 스냅샷과 입금을 같은 시계 단위 안에 기록하면
  입금 시각이 직전 측정 시각과 같아져 집계 구간에서 빠졌다. 시계가 넘어간 뒤
  입금하도록 했다.
국내주식 현금 주문이 아직 대체거래소(NXT) 도입 전 거래코드인 TTTC0802U·0801U를
쓰고 있었다. KIS 공식 예제(open-trading-api의 order_cash.py)는 매수 TTTC0012U,
매도 TTTC0011U, 모의투자는 VTTC0012U·VTTC0011U를 쓰고 거래소ID구분코드를
필수로 받는다. 같은 주문의 취소(TTTC0013U)는 이미 새 규격이라, 접수와 취소가
서로 다른 규격을 쓰고 있던 셈이다.

거래소 코드는 기본으로 KRX를 보낸다. 모의투자는 KRX 주문만 받으므로 다른 값은
요청을 보내기 전에 막는다. 이번에는 요청 구성만 테스트로 확인했고, 모의 서버로
실제 주문을 넣어 보는 것은 실전 전환 전에 한 번 해야 한다.
적립 계좌는 운용 규칙의 성과(시간가중)와 실제로 넣은 돈의 성과(금액가중)가
다르다. 자본시장연구원 26-04 보고서에서도 개인 계좌 가운데 수익을 낸 비율이
시간가중으로는 28%, 금액가중으로는 23%로 갈렸다. 지금까지는 시간가중만 보여 줬다.

평가 지표에 money_weighted_return_pct(Modified Dietz)와 넣은 돈 합계를 넣고,
주간 보고의 성과 줄에 같이 적는다. 판정에는 쓰지 않는다. 몇 달짜리 기록을
연율로 늘리면 과장되므로 연환산도 하지 않는다.

운영 DB 사본 기준 적립 계좌는 시간가중 -9.89%, 넣은 돈 40만 원 기준 -9.81%다.
10/15 삼성전자·현대차가 평균 매입가 -25% 손절로 팔렸는데, 같은 기간 KODEX 200도
-30% 가까이 빠져 있었다. 한 종목의 사고가 아니라 시장 전체 하락에 손절이 몰린 것이다.
8/07 이후 손절로 확정한 손실은 -819,468원(자본의 8.2%)이다. 그렇게 생긴 현금은
계좌 낙폭 15% 한도에 막혀 10/12부터 매일 매수가 거절됐고, 주식 비중은 설계 60%에서
45%까지 내려앉았다. 매도는 되는데 매수는 막히는 한쪽 방향 장치였다.

같은 9종목으로 다시 계산해 보니(tools/hold_policy_backtest.py) 지금까지 쓰던 조합
(평균 매입가 손절 + 낙폭 한도)이 두 구간 모두 가장 나빴다.

  2014-07~: 연 4.12%, 샤프 0.38 (손절 없음 5.58%, 0.48)
  2021-12~: 연 3.46%, 샤프 0.30 (손절 없음 5.99%, 0.45)

낙폭 한도만 걸어도 연 0.3~1.0%p가 깎였고, 평균 매입가 기준으로 손절한 종목은 그 뒤
60일 동안 시장보다 1~3%p 더 올랐다. 시장 대비 손절은 수익률이 손절 없음과 비슷하다.
수익을 올리는 장치가 아니라 한 종목 사고만 막는 장치로 남겨 둔다.

- stop_basis: relative
  같은 보유 기간의 KODEX 200 수익률보다 25% 넘게 뒤처지면 판다. 매입 시점의 시장
  수준은 매수 기록마다 그날 종가를 수량 가중 평균해서 구한다(평균 매입가와 같은 방식).
  지금 시장 가격을 모르면 그날은 판단을 미루고, 한 종목의 매수 기록만 맞지 않으면
  그 종목만 평균 매입가 기준으로 본다.
- account_drawdown_guard: delegate
  계좌 낙폭이 15%를 넘어도 정해 둔 주식 비중(60% + 3%p)까지는 다시 산다. 비중 상한은
  주문 단계에서 그대로 검사한다. 평가 화면의 매수 가능 상태도 같은 함수로 판단한다.

지금 들고 있는 6종목은 시장 대비 +7.7~+46.7%라 규칙을 바꾼 날 팔리는 종목은 없다.
운영 DB 사본으로 실제 주문 경로를 돌려 보니 낙폭 18%대에서도 보충 매수 2건이 체결돼
주식 비중이 44.6%에서 50.4%가 됐다. 손절한 3종목은 60일 재매수 제한이 풀려야
(11/20, 12/14) 60%까지 돌아간다.
10/12 적립 계좌는 주식 ETF 26.7%, CD ETF 61.2%(목표는 각각 47.5%)였는데 주문이
하나도 나오지 않았다. 종목마다 '목표 금액 - 보유 금액'을 1주 가격으로 내림하다 보니
부족분(약 7.9만 원)도 초과분(약 5.2만 원)도 0주가 됐다. CD ETF 2주를 팔아 KODEX 200
1주를 사면 주식 비중이 53.5%가 되고, 목표와의 차이는 20.8%p에서 6.0%p로 준다.

종목이 5개 이하인 바스켓은 planner: integer로 수량 조합을 함께 고른다
(core/integer_allocation.py). 종목마다 지금 수량, 목표 수량의 내림·올림, 내림-1을
후보로 두고, 현금 하한·최소 거래금액·회전 한도·재매수 제한을 지키는 조합 가운데
'위험자산 비중 차이 + 남는 현금 + 거래비용'이 가장 작은 것을 고른다. CD ETF와 현금은
위험이 거의 같아서 둘 사이의 차이는 따지지 않는다. 그래야 비용만 드는 CD 매도를 하지
않고, 적립금이 들어오면 팔지 않고 모자란 자산부터 산다.

2020-07~2026-10 과거 재생(tools/rebalance_review.py, 기존 계획기와 비교)에서는
연 12.53% → 12.56%, 최대 낙폭 -15.82% → -15.44%로 거의 같았다. 수익을 올리려는
변경이 아니라, 비중이 크게 어긋났는데도 주문이 0건인 상태를 없애려는 변경이다.

배분 진단도 손봤다. CD와 현금을 합친 안전 자산 비중이 목표 근처이고, 남는 현금으로
CD를 1주 더 살 수도 없다면 둘 사이의 차이는 경보로 세지 않는다. 1주 단위 때문에
생긴 차이라 손쓸 방법이 없기 때문이다.
바스켓 주문 가격은 데이터 공급자가 준 마지막 봉을 그대로 쓴다. 9/17~10/12처럼
공급자 자료가 멈추면 전날 가격으로 수량을 정하고, 모의 체결도 그 가격으로 남는다.

그래서 주문을 넣기 직전에 두 가지를 확인한다.
- 시세 날짜가 오늘이 아니면 주문하지 않는다. 날짜는 거래 시간 판정과 같은 시계로 본다.
- 전일 종가보다 30% 넘게 오르거나 내린 가격은 가격제한폭 밖이라 자료 오류로 보고
  주문하지 않는다.

주문을 보류하면 STALE_QUOTE_BLOCKED를 하루에 한 번 남긴다. 10/15~10/20 오전
측정은 모두 당일 시세였으므로 평소 운영을 막지는 않는다.

10/13 검증 도구(verify_basket_snapshot_fixes.py)는 10/12 종가로 10/13 주문을
재현하는 도구라, 그 안에서만 이번 점검과 이번 브랜치에서 바꾼 다른 규칙을 당시
기준으로 고정했다. 고정한 상태로 다시 돌려서 예전 결과가 그대로 나오는 것을 확인했다.
10월 급락 뒤 개인 투자자의 거래 실태, 많이 쓰는 자동매매 도구, 공개된 뒤에도 성과가
남은 운용 방식, 지금 시장 상황을 조사해 docs/MARKET_RESEARCH_20261020.md에 정리했다.
조사해 보니 새 매매 신호를 더할 근거는 없었고, 이미 정해 둔 규칙이 설계대로 돌게
고치는 쪽이 맞았다. 이번 브랜치의 변경이 그 결과다. 반영하지 않은 것(다자산 편입,
200TR 교체, 대형주 계좌 현금의 CD 운용, 같은 주식 비중의 비교지수)과 그 이유도 적었다.

tools/verify_policy_changes_20261020.py는 운영 DB를 읽기 전용으로 복사하고 시세를
고정한 뒤, 이번에 바꾼 규칙을 실제 계획 → 실행 경로로 돌려 본다. 원본 DB 해시가
그대로인지도 확인한다. README와 프로젝트 안내서에 새 문서와 설정 항목을 연결했다.
@easygap
easygap merged commit 3e608b6 into main Oct 20, 2026
1 check failed
@easygap
easygap deleted the feat/market-research-improvements-20261020 branch October 20, 2026 08:49
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