S&P500 · 적립식 · 매수 시점 · 실측 재현 페이지

S&P500 적립식,
바로 살까 폭락 기다릴까

S&P500 지수 펀드(VFINX)의 1985년부터 실제 가격(배당 재투자)으로, 매달 첫 거래일에 50만원씩 10년 동안 넣는 계좌를 둘 만들었습니다. ① 바로 — 그날 종가에 전부 산다 ② 기다리기 — 그 돈을 현금(미국 3개월 국채 이자)으로 모아 두다가, 전날 종가가 그때까지 가장 높았던 종가보다 20% 넘게 낮은 첫날 모은 돈을 전부 산다(기다리는 기간 상한 없음). 시작 달을 한 달씩 밀어 이런 10년짜리 계좌를 382번 만들었습니다(서로 많이 겹칩니다).

290번382번 중 월급날 바로 산 쪽이 10년 뒤 더 많이 남은 경우 (기다린 쪽 92번)
1억 2,343만원두 계좌가 남긴 돈의 비율 순으로 줄 세운 가운데(192번째) 계좌(2014년 4월부터 10년 · 넣은 돈 6,000만원) — 바로. 기다리기는 1억 888만원
0가지기다리는 폭·기간·절반만 기다리기를 바꾼 50가지 중 382번의 절반 넘게 이긴 방법 (가장 많이 이긴 방법 155번)

오늘 드리는 것 — 기준 한 줄

상황기준
S&P500 에 매달 적립한다떨어지길 기다리지 말고 월급날(매달 첫 거래일) 바로 산다 — 382번 중 290번 더 남음
기다린 쪽이 이긴 때 · 진 때기다린 쪽이 이긴 92번 중 가장 크게 이긴 계좌 +553만원 · 진 290번 중 가장 크게 진 계좌 -1억 1,130만원 (10년 동안 넣은 돈은 똑같이 6,000만원) — 영상의 「넷 중 셋」은 382번 중 290번입니다
지키는 법증권사 앱의 자동 모으기(적립식 자동 매수) 메뉴에서 날짜는 월급날, 금액은 정해 둔 만큼, 종목은 S&P500 ETF 로 걸어 둡니다 — 무서운 뉴스가 나온 날에도 그날 그대로 삽니다
조금만 떨어져도 사고 오래는 안 기다리면(−10% · 최대 12개월)바로 302번 · 기다리기 80번 — 기다리는 방법 50가지 중 절반 넘게 이긴 것 0가지
왜기다리는 동안에도 대개 올라, 떨어진 값이 월급날 값보다 높은 때가 많았습니다 — 1985년부터 기다렸다 산 455달 중 249달이 월급날보다 비쌌습니다(싸게 131달 · 같은 날 75달). 가운데 계좌는 2014년 4월부터 모은 72달치를 2020-03-13(코로나 폭락) 한꺼번에 샀는데 그중 55달은 월급날 값이 더 쌌고, 마지막 16달치는 끝날까지 현금이었습니다. 10년 내내 한 번도 못 산 계좌 30개

한계 — 먼저 읽어 주세요

받아가기

돌리는 법: zip 을 풀고 pip install numpy pandas 뒤 python run_dipwait.py (몇 분 걸립니다).

그대로 돌린 출력

자료: VFINX 1985-01-02~2026-10-09 · ^IRX 끝 2026-10-12 · ASOF 2026-10-12

[S&P500 · 매달 50만원 · 20% 떨어지면 사기(상한 없음)] 10년짜리 계좌 382개 (1985-01-02~1994-12-30 … 2016-10-03~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 290번 · 기다리기 92번 · 같음 0번
  가운데 계좌(192/382): 2014-04-01~2024-03-28 · 넣은 돈 6,000만원 → 바로 1억 2,343만원 · 기다리기 1억 888만원
  기다린 돈이 산 날까지 평균 809.7거래일 · 1년 넘게 기다린 돈 65.7%
  10년 내내 한 번도 못 산 계좌 30개 · 끝날 현금이 남은 계좌 302개
  기다리기가 가장 크게 이긴 계좌 +553만원 · 가장 크게 진 계좌 -1억 1,130만원
  1985년부터 넣은 달: 기다렸다 산 달 455개 중 월급날 값보다 비싸게 산 달 249개 (54.7%) · 싸게 산 달 131개 · 같은 날 산 달 75개 · 아직 못 산 달 47개

[가운데 계좌 리플레이] 2014-04-01~2024-03-28
  2020-03-13 샀다(전날 최고점 대비 -26.7%) — 2014-04-01~2020-03-02 에 모은 72달치 · 그중 월급날 값이 더 쌌던 달 55개
  2020-04-01 샀다(전날 최고점 대비 -23.5%) — 2020-04-01~2020-04-01 에 모은 1달치 · 그중 월급날 값이 더 쌌던 달 0개
  2022-06-14 샀다(전날 최고점 대비 -21.3%) — 2020-05-01~2022-06-01 에 모은 26달치 · 그중 월급날 값이 더 쌌던 달 10개
  2022-07-01 샀다(전날 최고점 대비 -20.5%) — 2022-07-01~2022-07-01 에 모은 1달치 · 그중 월급날 값이 더 쌌던 달 0개
  2022-09-22 샀다(전날 최고점 대비 -20.2%) — 2022-08-01~2022-09-01 에 모은 2달치 · 그중 월급날 값이 더 쌌던 달 0개
  2022-10-03 샀다(전날 최고점 대비 -24.4%) — 2022-10-03~2022-10-03 에 모은 1달치 · 그중 월급날 값이 더 쌌던 달 0개
  2022-11-03 샀다(전날 최고점 대비 -20.7%) — 2022-11-01~2022-11-01 에 모은 1달치 · 그중 월급날 값이 더 쌌던 달 0개
  끝까지 현금: 2022-12-01~2024-03-01 에 모은 16달치
  산 달 104개 중 월급날보다 비싸게 산 달 65개

[반론 · 10% 떨어지면 사기 · 최대 12개월] 10년짜리 계좌 382개 (1985-01-02~1994-12-30 … 2016-10-03~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 302번 · 기다리기 80번 · 같음 0번
  가운데 계좌(192/382): 1987-07-01~1997-06-30 · 넣은 돈 6,000만원 → 바로 1억 5,593만원 · 기다리기 1억 5,069만원

[반론 · 기다리는 방법 50가지] 382개 계좌 중 절반 넘게 이긴 방법 0가지 · 가장 많이 이긴 방법 155번(−5% · 최대 24개월 · 전부)
  − 5% · 최대   3개월 · 전부: 기다리기가 더 남은 113/382 · 가운데 비율 0.9973
  − 5% · 최대   3개월 · 절반: 기다리기가 더 남은 113/382 · 가운데 비율 0.9986
  − 5% · 최대   6개월 · 전부: 기다리기가 더 남은 117/382 · 가운데 비율 0.9973
  − 5% · 최대   6개월 · 절반: 기다리기가 더 남은 117/382 · 가운데 비율 0.9986
  − 5% · 최대  12개월 · 전부: 기다리기가 더 남은 124/382 · 가운데 비율 0.9965
  − 5% · 최대  12개월 · 절반: 기다리기가 더 남은 124/382 · 가운데 비율 0.9983
  − 5% · 최대  24개월 · 전부: 기다리기가 더 남은 155/382 · 가운데 비율 0.9988
  − 5% · 최대  24개월 · 절반: 기다리기가 더 남은 155/382 · 가운데 비율 0.9994
  − 5% · 최대 inf개월 · 전부: 기다리기가 더 남은 155/382 · 가운데 비율 0.9988
  − 5% · 최대 inf개월 · 절반: 기다리기가 더 남은 155/382 · 가운데 비율 0.9994
  −10% · 최대   3개월 · 전부: 기다리기가 더 남은  60/382 · 가운데 비율 0.9896
  −10% · 최대   3개월 · 절반: 기다리기가 더 남은  60/382 · 가운데 비율 0.9948
  −10% · 최대   6개월 · 전부: 기다리기가 더 남은  70/382 · 가운데 비율 0.9799
  −10% · 최대   6개월 · 절반: 기다리기가 더 남은  70/382 · 가운데 비율 0.9900
  −10% · 최대  12개월 · 전부: 기다리기가 더 남은  80/382 · 가운데 비율 0.9664
  −10% · 최대  12개월 · 절반: 기다리기가 더 남은  80/382 · 가운데 비율 0.9832
  −10% · 최대  24개월 · 전부: 기다리기가 더 남은  73/382 · 가운데 비율 0.9586
  −10% · 최대  24개월 · 절반: 기다리기가 더 남은  73/382 · 가운데 비율 0.9793
  −10% · 최대 inf개월 · 전부: 기다리기가 더 남은  73/382 · 가운데 비율 0.9545
  −10% · 최대 inf개월 · 절반: 기다리기가 더 남은  73/382 · 가운데 비율 0.9772
  −15% · 최대   3개월 · 전부: 기다리기가 더 남은  66/382 · 가운데 비율 0.9860
  −15% · 최대   3개월 · 절반: 기다리기가 더 남은  66/382 · 가운데 비율 0.9930
  −15% · 최대   6개월 · 전부: 기다리기가 더 남은  68/382 · 가운데 비율 0.9744
  −15% · 최대   6개월 · 절반: 기다리기가 더 남은  68/382 · 가운데 비율 0.9872
  −15% · 최대  12개월 · 전부: 기다리기가 더 남은  83/382 · 가운데 비율 0.9564
  −15% · 최대  12개월 · 절반: 기다리기가 더 남은  83/382 · 가운데 비율 0.9782
  −15% · 최대  24개월 · 전부: 기다리기가 더 남은 104/382 · 가운데 비율 0.9374
  −15% · 최대  24개월 · 절반: 기다리기가 더 남은 104/382 · 가운데 비율 0.9687
  −15% · 최대 inf개월 · 전부: 기다리기가 더 남은 101/382 · 가운데 비율 0.9319
  −15% · 최대 inf개월 · 절반: 기다리기가 더 남은 101/382 · 가운데 비율 0.9660
  −20% · 최대   3개월 · 전부: 기다리기가 더 남은  49/382 · 가운데 비율 0.9832
  −20% · 최대   3개월 · 절반: 기다리기가 더 남은  49/382 · 가운데 비율 0.9916
  −20% · 최대   6개월 · 전부: 기다리기가 더 남은  54/382 · 가운데 비율 0.9685
  −20% · 최대   6개월 · 절반: 기다리기가 더 남은  54/382 · 가운데 비율 0.9843
  −20% · 최대  12개월 · 전부: 기다리기가 더 남은  57/382 · 가운데 비율 0.9410
  −20% · 최대  12개월 · 절반: 기다리기가 더 남은  57/382 · 가운데 비율 0.9705
  −20% · 최대  24개월 · 전부: 기다리기가 더 남은  67/382 · 가운데 비율 0.9099
  −20% · 최대  24개월 · 절반: 기다리기가 더 남은  67/382 · 가운데 비율 0.9550
  −20% · 최대 inf개월 · 전부: 기다리기가 더 남은  92/382 · 가운데 비율 0.8811
  −20% · 최대 inf개월 · 절반: 기다리기가 더 남은  92/382 · 가운데 비율 0.9406
  −30% · 최대   3개월 · 전부: 기다리기가 더 남은  52/382 · 가운데 비율 0.9834
  −30% · 최대   3개월 · 절반: 기다리기가 더 남은  52/382 · 가운데 비율 0.9917
  −30% · 최대   6개월 · 전부: 기다리기가 더 남은  63/382 · 가운데 비율 0.9691
  −30% · 최대   6개월 · 절반: 기다리기가 더 남은  63/382 · 가운데 비율 0.9846
  −30% · 최대  12개월 · 전부: 기다리기가 더 남은  66/382 · 가운데 비율 0.9471
  −30% · 최대  12개월 · 절반: 기다리기가 더 남은  66/382 · 가운데 비율 0.9736
  −30% · 최대  24개월 · 전부: 기다리기가 더 남은  80/382 · 가운데 비율 0.9229
  −30% · 최대  24개월 · 절반: 기다리기가 더 남은  80/382 · 가운데 비율 0.9614
  −30% · 최대 inf개월 · 전부: 기다리기가 더 남은 128/382 · 가운데 비율 0.9372
  −30% · 최대 inf개월 · 절반: 기다리기가 더 남은 128/382 · 가운데 비율 0.9686

[반론 · 기간을 5년으로 · 20% 떨어지면 사기] 5년짜리 계좌 442개 (1985-01-02~1989-12-29 … 2021-10-01~2026-09-30)
  5년 뒤 더 많이 남은 쪽: 월급날 바로 286번 · 기다리기 156번 · 같음 0번
  가운데 계좌(222/442): 2009-02-02~2014-01-31 · 넣은 돈 3,000만원 → 바로 4,600만원 · 기다리기 4,351만원
[반론 · 기간을 15년으로 · 20% 떨어지면 사기] 15년짜리 계좌 322개 (1985-01-02~1999-12-31 … 2011-10-03~2026-09-30)
  15년 뒤 더 많이 남은 쪽: 월급날 바로 266번 · 기다리기 56번 · 같음 0번
  가운데 계좌(162/322): 2007-11-01~2022-10-31 · 넣은 돈 9,000만원 → 바로 2억 3,163만원 · 기다리기 2억 442만원
[반론 · 기간을 20년으로 · 20% 떨어지면 사기] 20년짜리 계좌 262개 (1985-01-02~2004-12-31 … 2006-10-02~2026-09-30)
  20년 뒤 더 많이 남은 쪽: 월급날 바로 262번 · 기다리기 0번 · 같음 0번
  가운데 계좌(132/262): 2005-12-01~2025-11-28 · 넣은 돈 1억 2,000만원 → 바로 5억 3,376만원 · 기다리기 4억 9,103만원

[한계 · 수수료·세금 어림] VFINX 원래 종가 + 배당 기록으로 다시 지은 총수익 · 10년짜리 계좌
  기준_원래종가재구성: 월급날 바로 290번 · 기다리기 92번 (계좌 382개)
  수수료0.25: 월급날 바로 290번 · 기다리기 92번 (계좌 382개)
  이자세15.4: 월급날 바로 300번 · 기다리기 82번 (계좌 382개)
  배당세15: 월급날 바로 281번 · 기다리기 101번 (계좌 382개)
  셋다: 월급날 바로 287번 · 기다리기 95번 (계좌 382개)
  (수수료 = 산 몫의 0.25% · 이자세 = 현금 이자의 15.4% · 배당세 = 배당의 15%)

※ 야후 069500.KS 는 2007-02-07 다음 봉이 2009-04-17 이다(2008년 전체 공백) — 코스피200 ETF 는 2009-05 부터만 쓴다
[한계 · 코스피200 ETF(KODEX200) · 2009-05~ · 원화 · 현금 이자 0%] 10년짜리 계좌 90개 (2009-05-04~2019-04-30 … 2016-10-04~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 0번 · 기다리기 90번 · 같음 0번
  가운데 계좌(46/90): 2011-10-04~2021-09-30 · 넣은 돈 6,000만원 → 바로 9,661만원 · 기다리기 1억 193만원
  기다렸다 산 달 210개 중 월급날 값보다 싸게 산 달 129개 · 비싸게 산 달 56개 · 같은 날 산 달 25개
[한계 · 코스피200 ETF(KODEX200) · 2009-05~ · 원화 · 현금 이자 3%] 10년짜리 계좌 90개 (2009-05-04~2019-04-30 … 2016-10-04~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 0번 · 기다리기 90번 · 같음 0번
  가운데 계좌(46/90): 2011-10-04~2021-09-30 · 넣은 돈 6,000만원 → 바로 9,661만원 · 기다리기 1억 488만원
[한계 · 코스피200 ETF(TIGER200) · 2009-05~ · 원화 · 현금 이자 0%] 10년짜리 계좌 90개 (2009-05-04~2019-04-30 … 2016-10-04~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 0번 · 기다리기 90번 · 같음 0번
  가운데 계좌(46/90): 2011-10-04~2021-09-30 · 넣은 돈 6,000만원 → 바로 9,743만원 · 기다리기 1억 306만원
[한계 · 코스피 지수(가격지수 · 배당 0%) · 1997~ · 현금 이자 0%] 10년짜리 계좌 238개 (1997-01-03~2006-12-28 … 2016-10-04~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 121번 · 기다리기 117번 · 같음 0번
  가운데 계좌(120/238): 2005-08-01~2015-07-31 · 넣은 돈 6,000만원 → 바로 7,267만원 · 기다리기 7,237만원
[한계 · 코스피 지수(가격지수 · 배당 0%) · 1997~ · 현금 이자 3%] 10년짜리 계좌 238개 (1997-01-03~2006-12-28 … 2016-10-04~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 107번 · 기다리기 131번 · 같음 0번
  가운데 계좌(120/238): 2008-09-01~2018-08-31 · 넣은 돈 6,000만원 → 바로 7,411만원 · 기다리기 7,582만원
[한계 · 코스피 지수 + 배당 연 1.8%(어림) · 1997~ · 현금 이자 0%] 10년짜리 계좌 238개 (1997-01-03~2006-12-28 … 2016-10-04~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 174번 · 기다리기 64번 · 같음 0번
  가운데 계좌(120/238): 2011-10-05~2021-09-30 · 넣은 돈 6,000만원 → 바로 9,529만원 · 기다리기 9,225만원
[한계 · 코스피 지수 + 배당 연 1.8%(어림) · 1997~ · 현금 이자 3%] 10년짜리 계좌 238개 (1997-01-03~2006-12-28 … 2016-10-04~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 122번 · 기다리기 116번 · 같음 0번
  가운데 계좌(120/238): 1997-03-03~2007-02-28 · 넣은 돈 6,000만원 → 바로 1억 3,435만원 · 기다리기 1억 3,346만원
[한계 · S&P500 원화로(SPY × 원달러) · 현금 이자 0%] 10년짜리 계좌 155개 (2003-12-01~2013-11-29 … 2016-10-03~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 155번 · 기다리기 0번 · 같음 0번
  가운데 계좌(78/155): 2013-02-01~2023-01-31 · 넣은 돈 6,000만원 → 바로 1억 1,838만원 · 기다리기 8,665만원
[한계 · S&P500 원화로(SPY × 원달러) · 현금 이자 3%] 10년짜리 계좌 155개 (2003-12-01~2013-11-29 … 2016-10-03~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 155번 · 기다리기 0번 · 같음 0번
  가운데 계좌(78/155): 2016-02-01~2026-01-30 · 넣은 돈 6,000만원 → 바로 1억 6,206만원 · 기다리기 1억 2,644만원
[한계 · 나스닥100(QQQ) · 미국 단기 국채 이자] 10년짜리 계좌 212개 (1999-03-10~2009-02-27 … 2016-10-03~2026-09-30)
  10년 뒤 더 많이 남은 쪽: 월급날 바로 151번 · 기다리기 21번 · 같음 40번
  가운데 계좌(107/212): 2007-03-01~2017-02-28 · 넣은 돈 6,000만원 → 바로 1억 4,012만원 · 기다리기 1억 3,644만원

끝.

코드 전문

# -*- coding: utf-8 -*-
"""AlgoLab 재현 코드 — S&P500 적립식, 월급날 바로 살까 떨어질 때까지 모았다 살까 (algolab.co.kr/dipwait)
ASOF = 2026-10-12 (얼린 자료의 마지막 날). 돌리는 법: pip install numpy pandas → python run_dipwait.py

설계:
- 자료: 야후 VFINX(뱅가드 S&P500 인덱스펀드) 수정종가(배당 재투자) 1985-01 ~ · 달러 기준
- 매달 첫 거래일에 50만원(달러 기준 비율을 만원으로 환산 · 환율 미반영)
  ① 바로: 그날 종가에 전부 산다
  ② 기다리기(−20%): 그달 돈을 현금으로 모아 둔다. 전날 종가가 그때까지의 최고 종가보다 20% 이상
     낮은 첫 거래일 종가에 모아 둔 돈을 전부 산다. 기다리는 기간 상한 없음.
     모아 둔 현금에는 미국 3개월 국채 금리(^IRX) 이자를 붙인다.
- 10년 창: 시작 달을 한 달씩 밀며 120번 넣는다. 끝 날 = 120번째 넣은 다음 달 첫 거래일 전날.
  끝 날 계좌 = 주식 평가액 + 아직 안 쓴 현금(이자 포함).
- 반론 ① 짧게: −10% · 최대 12개월 / 반론 ② 기다리는 방법 50가지(−5·10·15·20·30% × 최대 3·6·12·24개월·무한 × 전부/절반)
- 반론 ③ 기간: 5·15·20년 계좌
- 한계: 코스피200 ETF(KODEX200·TIGER200 · 2009-05~ · 현금 이자 0%·3%) · 코스피 지수(1997~ · 배당 0%/연 1.8% 어림 × 이자 0%/3%)
        · S&P500 원화(SPY × 원달러 · 2003-12~) · 나스닥100(QQQ)
        · 수수료·세금 어림(수수료 0.25% · 현금 이자세 15.4% · 배당세 15%)
"""
import json
import os
import sys

import numpy as np
import pandas as pd

sys.stdout.reconfigure(encoding="utf-8", errors="replace")
HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data_dipwait")
ASOF = "2026-10-12"
MONTHLY = 50          # 만원
H = 120               # 10년


def load(name):
    df = pd.read_csv(os.path.join(DATA, name + ".csv"), parse_dates=["date"])
    df = df[df["date"] <= ASOF]
    s = df.set_index("date")["close"].astype(float)
    return s[s > 0]


IRX = load("_IRX")
RAWNAME, DIVNAME, ASOF_RAW = "VFINX_raw.csv", "VFINX_div.csv", None


def cash_index(idx, mode):
    if mode == "irx":
        r = IRX.reindex(idx, method="ffill").bfill().values / 100.0
        daily = (1 + r) ** (1 / 252.0)
    else:
        daily = np.full(len(idx), (1 + mode) ** (1 / 252.0))
    daily[0] = 1.0
    return np.cumprod(daily)


def month_starts(idx):
    s = pd.Series(np.arange(len(idx)), index=idx)
    return s.groupby([idx.year, idx.month]).first().values


def deploy_days(cond, contrib, K, n):
    nxt = np.full(n + 1, n, dtype=np.int64)
    for t in range(n - 1, -1, -1):
        nxt[t] = t if cond[t] else nxt[t + 1]
    out = []
    for i, c in enumerate(contrib):
        d = nxt[c]
        if K is not None:
            j = i + K
            cap = contrib[j] if j < len(contrib) else n
            d = min(d, cap)
        out.append(d)
    return np.array(out)


class Market:
    def __init__(self, price, cash_mode, start="1985-01-01"):
        p = price[price.index >= start]
        self.idx = p.index
        self.price = p.values
        self.n = len(self.price)
        self.C = cash_index(self.idx, cash_mode)
        self.contrib = month_starts(self.idx)
        runmax = np.maximum.accumulate(self.price)
        dd = self.price / runmax - 1
        self.dd_prev = np.concatenate([[0.0], dd[:-1]])   # 전날 종가 기준(룩어헤드 없음)
        self.nwin = len(self.contrib) - H
        self.E = np.array([self.contrib[w + H] - 1 for w in range(self.nwin)])

    def values(self, d):
        """창마다 끝 날 계좌(1 단위 납입 기준) · 창마다 끝까지 안 쓴 현금 비중."""
        n, C, price, contrib = self.n, self.C, self.price, self.contrib
        vals = np.zeros(self.nwin)
        cashfrac = np.zeros(self.nwin)
        for w in range(self.nwin):
            e = self.E[w]
            cs = contrib[w:w + H]
            ds = d[w:w + H]
            bought = ds <= e
            dd_ = np.minimum(ds, n - 1)
            stock = np.where(bought, (C[dd_] / C[cs]) / price[dd_] * price[e], 0.0)
            cash = np.where(bought, 0.0, C[e] / C[cs])
            vals[w] = stock.sum() + cash.sum()
            cashfrac[w] = cash.sum() / vals[w]
        return vals, cashfrac

    def arm(self, X, K):
        if X is None:
            return self.contrib.copy()
        return deploy_days(self.dd_prev <= -X / 100.0, self.contrib, K, self.n)

    def date(self, i):
        return str(self.idx[min(i, self.n - 1)].date())


def compare(mk, X, K, half=False):
    A, _ = mk.values(mk.arm(None, None))
    d = mk.arm(X, K)
    B, cf = mk.values(d)
    if half:
        B = 0.5 * A + 0.5 * B
    r = B / A
    order = np.argsort(r)
    mid = int(order[len(order) // 2])
    wait = np.minimum(d, mk.n - 1) - mk.contrib
    return {
        "창수": int(mk.nwin),
        "첫창": mk.date(mk.contrib[0]) + "~" + mk.date(mk.E[0]),
        "끝창": mk.date(mk.contrib[mk.nwin - 1]) + "~" + mk.date(mk.E[-1]),
        "기다림이더남음": int((B > A).sum()),
        "바로가더남음": int((B < A).sum()),
        "같음": int((B == A).sum()),
        "한가운데": {
            "창": mk.date(mk.contrib[mid]) + "~" + mk.date(mk.E[mid]),
            "순위": f"{len(order) // 2 + 1}/{len(order)}",
            "넣은돈_만원": MONTHLY * H,
            "바로_만원": int(round(A[mid] * MONTHLY)),
            "기다림_만원": int(round(B[mid] * MONTHLY)),
            "끝날현금비중": round(float(cf[mid]), 3),
        },
        "기다림_가장많이이김_만원": int(round(((B - A) * MONTHLY).max())),
        "기다림_가장많이짐_만원": int(round(((B - A) * MONTHLY).min())),
        "평균대기_거래일": round(float(wait.mean()), 1),
        "1년넘게기다린달_비율": round(float((wait > 252).mean()), 3),
        "끝까지한번도안산창": int(sum(1 for w in range(mk.nwin) if (d[w:w + H] > mk.E[w]).all())),
        "끝날현금이남은창": int((cf > 0).sum()),
        "_r": r, "_A": A * MONTHLY, "_B": B * MONTHLY,
    }


def paid_more(mk, X=20):
    """기다렸다 산 달 가운데, 산 값이 그달 첫 거래일(월급날) 값보다 비쌌던 달 — 1985~ 전체 달 기준."""
    d = mk.arm(X, None)
    ok = d < mk.n
    pc = mk.price[mk.contrib[ok]]
    pb = mk.price[d[ok]]
    return {"산달": int(ok.sum()), "아직못산달": int((~ok).sum()), "월급날보다비싸게산달": int((pb > pc).sum()),
            "월급날보다싸게산달": int((pb < pc).sum()), "같은날산달": int((pb == pc).sum()),
            "비율": round(float((pb > pc).mean()), 3)}


def replay(mk, start_ym, X=20):
    """한 계좌의 거래 리플레이 — 어느 달 돈이 언제 들어갔나, 들어간 값이 월급날 값보다 비쌌나."""
    d = mk.arm(X, None)
    w = next(i for i in range(mk.nwin) if mk.date(mk.contrib[i]).startswith(start_ym))
    e = mk.E[w]
    cs, ds = mk.contrib[w:w + H], d[w:w + H]
    buys = []
    for day in sorted(set(int(x) for x in ds if x <= e)):
        months = [int(ci) for ci, di in zip(cs, ds) if di == day]
        p = mk.price[day]
        buys.append({"산날": mk.date(day), "달수": len(months), "첫달": mk.date(months[0]), "끝달": mk.date(months[-1]),
                     "전날낙폭": round(float(mk.dd_prev[day]), 3),
                     "월급날값이더쌌던달": sum(1 for ci in months if mk.price[ci] < p)})
    left = [int(ci) for ci, di in zip(cs, ds) if di > e]
    return {"창": mk.date(cs[0]) + "~" + mk.date(e), "산날들": buys,
            "끝까지현금": {"달수": len(left), "첫달": mk.date(left[0]) if left else None,
                        "끝달": mk.date(left[-1]) if left else None},
            "산달": int(sum(b["달수"] for b in buys)),
            "월급날보다비싸게산달": int(sum(1 for ci, di in zip(cs, ds) if di <= e and mk.price[di] > mk.price[ci]))}


def compare_cost(price_tr, irx_scale, fee, X=20):
    """수수료(산 몫에 fee) · 현금 이자세(이자 × irx_scale) · 배당세(price_tr 로) 를 넣은 판 — 바로/기다리기 승수만."""
    global IRX
    keep = IRX
    IRX = keep * irx_scale
    try:
        mk = Market(price_tr, "irx")
    finally:
        IRX = keep
    res = []
    for d in (mk.arm(None, None), mk.arm(X, None)):
        vals = np.zeros(mk.nwin)
        for w in range(mk.nwin):
            e = mk.E[w]
            cs, ds = mk.contrib[w:w + H], d[w:w + H]
            b = ds <= e
            dd = np.minimum(ds, mk.n - 1)
            st = np.where(b, (mk.C[dd] / mk.C[cs]) / mk.price[dd] * mk.price[e] * (1 - fee), 0.0)
            ca = np.where(b, 0.0, mk.C[e] / mk.C[cs])
            vals[w] = st.sum() + ca.sum()
        res.append(vals)
    A, B = res
    return {"창수": int(mk.nwin), "바로가더남음": int((A > B).sum()), "기다림이더남음": int((B > A).sum())}


def after_tax_tr(tax):
    """VFINX 원래 종가 + 배당(야후 events) 로 배당세를 뗀 총수익 지수를 짓는다."""
    raw = pd.read_csv(os.path.join(DATA, RAWNAME), parse_dates=["date"]).set_index("date")
    raw = raw[raw.index <= ASOF_RAW] if ASOF_RAW else raw
    dv = pd.read_csv(os.path.join(DATA, DIVNAME), parse_dates=["date"]).set_index("date")["amount"]
    P = raw["close"].astype(float)
    D = dv.reindex(P.index).fillna(0.0)
    r = (P + D * (1 - tax)) / P.shift(1)
    r.iloc[0] = 1.0
    return pd.Series(np.cumprod(r.values), index=P.index)


def strip(x):
    return {k: v for k, v in x.items() if not k.startswith("_")}


def main():
    global H
    out = {"입력": {"매달_만원": MONTHLY, "기간_년": H // 12, "넣은돈_만원": MONTHLY * H,
                    "기다림_문턱": 20, "짧은기다림_문턱": 10, "짧은기다림_최대개월": 12}}
    vf = Market(load("VFINX"), "irx")
    out["자료"] = {"VFINX": f"{vf.date(0)}~{vf.date(vf.n - 1)}", "IRX끝": str(IRX.index[-1].date())}

    main_ = compare(vf, 20, None)
    out["본편_SP500_기다림20"] = strip(main_)
    short = compare(vf, 10, 12)
    out["반론_짧게_10_12개월"] = strip(short)
    # 반론 ③ 10년이 아니면? — 같은 규칙을 5·15·20년 계좌로
    for yrs in (5, 15, 20):
        H = yrs * 12
        out[f"반론_기간_{yrs}년"] = strip(compare(Market(load("VFINX"), "irx"), 20, None))
    H = 120
    out["왜_비싸게산달_SP500"] = paid_more(vf)
    out["리플레이_한가운데"] = replay(vf, main_["한가운데"]["창"][:7])

    # 한계 ④ 수수료·세금 어림 — 수수료 0.25%(산 몫) · 현금 이자세 15.4% · 미국 배당세 15%
    tr15 = after_tax_tr(0.15)
    tr0 = after_tax_tr(0.0)
    out["한계_비용"] = {
        "기준_원래종가재구성": compare_cost(tr0[tr0.index >= "1985-01-01"], 1.0, 0.0),
        "수수료0.25": compare_cost(tr0[tr0.index >= "1985-01-01"], 1.0, 0.0025),
        "이자세15.4": compare_cost(tr0[tr0.index >= "1985-01-01"], 0.846, 0.0),
        "배당세15": compare_cost(tr15[tr15.index >= "1985-01-01"], 1.0, 0.0),
        "셋다": compare_cost(tr15[tr15.index >= "1985-01-01"], 0.846, 0.0025),
    }

    # 50가지
    cells = []
    for X in (5, 10, 15, 20, 30):
        for K in (3, 6, 12, 24, None):
            for half in (False, True):
                c = compare(vf, X, K, half)
                cells.append({"X": X, "K": K if K else "inf", "절반": half,
                              "기다림이더남음": c["기다림이더남음"], "창수": c["창수"],
                              "중앙비율": round(float(np.median(c["_r"])), 4)})
    out["반론_50가지"] = {
        "가지수": len(cells),
        "절반넘게이긴가지": sum(1 for c in cells if c["기다림이더남음"] > c["창수"] / 2),
        "가장많이이긴가지": max(cells, key=lambda c: c["기다림이더남음"]),
        "중앙비율1넘은가지": sum(1 for c in cells if c["중앙비율"] > 1),
        "cells": cells,
    }

    # 한계: 코스피200 ETF · 원화 S&P500 · 나스닥100
    # ⚠ 야후 069500.KS 는 2007-02-07 다음 봉이 2009-04-17 이다(2008년 전체 공백) — 2009-05 부터만 쓴다(261013 심사 지적)
    kx = load("069500_KS")
    out["한계_KODEX200_이자0"] = strip(compare(Market(kx, 0.0, "2009-05-01"), 20, None))
    out["왜_비싸게산달_KODEX200"] = paid_more(Market(kx, 0.0, "2009-05-01"))
    out["한계_KODEX200_이자3"] = strip(compare(Market(kx, 0.03, "2009-05-01"), 20, None))
    out["한계_TIGER200_이자0"] = strip(compare(Market(load("102110_KS"), 0.0, "2009-05-01"), 20, None))
    # 길게: 코스피 지수(^KS11 · 가격지수라 배당 없음) 1997~ — 배당 0%·연 1.8%(어림) × 현금 이자 0%·3%
    k11 = load("_KS11")
    yrs = np.arange(len(k11)) / 252.0
    k11d = pd.Series(k11.values * np.exp(np.log1p(0.018) * yrs), index=k11.index)
    out["한계_코스피지수_배당0_이자0"] = strip(compare(Market(k11, 0.0, "1997-01-01"), 20, None))
    out["한계_코스피지수_배당0_이자3"] = strip(compare(Market(k11, 0.03, "1997-01-01"), 20, None))
    out["한계_코스피지수_배당18_이자0"] = strip(compare(Market(k11d, 0.0, "1997-01-01"), 20, None))
    out["한계_코스피지수_배당18_이자3"] = strip(compare(Market(k11d, 0.03, "1997-01-01"), 20, None))
    spy = load("SPY")
    fx = load("USDKRW_X")
    j = spy.index.intersection(fx.index)
    spy_krw = (spy.reindex(j) * fx.reindex(j)).dropna()
    out["한계_SP500원화_이자0"] = strip(compare(Market(spy_krw, 0.0, "2003-12-01"), 20, None))
    out["한계_SP500원화_이자3"] = strip(compare(Market(spy_krw, 0.03, "2003-12-01"), 20, None))
    out["한계_나스닥100"] = strip(compare(Market(load("QQQ"), "irx", "1999-01-01"), 20, None))


    def won(v):
        v = int(v)
        if abs(v) >= 10000:
            r = abs(v) % 10000
            s = f"{abs(v) // 10000}억" + (f" {r:,}만원" if r else "원")
            return ("-" if v < 0 else "") + s
        return f"{v:,}만원"

    def line(name, v, yrs=10):
        g = v["한가운데"]
        print(f"[{name}] {yrs}년짜리 계좌 {v['창수']:,}개 ({v['첫창']} … {v['끝창']})")
        print(f"  {yrs}년 뒤 더 많이 남은 쪽: 월급날 바로 {v['바로가더남음']:,}번 · 기다리기 {v['기다림이더남음']:,}번 · 같음 {v['같음']}번")
        print(f"  가운데 계좌({g['순위']}): {g['창']} · 넣은 돈 {won(g['넣은돈_만원'])} → 바로 {won(g['바로_만원'])} · 기다리기 {won(g['기다림_만원'])}")

    print(f"자료: VFINX {out['자료']['VFINX']} · ^IRX 끝 {out['자료']['IRX끝']} · ASOF {ASOF}\n")
    m = out["본편_SP500_기다림20"]
    line("S&P500 · 매달 50만원 · 20% 떨어지면 사기(상한 없음)", m)
    print(f"  기다린 돈이 산 날까지 평균 {m['평균대기_거래일']}거래일 · 1년 넘게 기다린 돈 {m['1년넘게기다린달_비율'] * 100:.1f}%")
    print(f"  10년 내내 한 번도 못 산 계좌 {m['끝까지한번도안산창']}개 · 끝날 현금이 남은 계좌 {m['끝날현금이남은창']}개")
    print(f"  기다리기가 가장 크게 이긴 계좌 +{won(m['기다림_가장많이이김_만원'])} · 가장 크게 진 계좌 {won(m['기다림_가장많이짐_만원'])}")
    pm = out["왜_비싸게산달_SP500"]
    print(f"  1985년부터 넣은 달: 기다렸다 산 달 {pm['산달']}개 중 월급날 값보다 비싸게 산 달 {pm['월급날보다비싸게산달']}개 ({pm['비율'] * 100:.1f}%)"
          f" · 싸게 산 달 {pm['월급날보다싸게산달']}개 · 같은 날 산 달 {pm['같은날산달']}개 · 아직 못 산 달 {pm['아직못산달']}개")
    rp = out["리플레이_한가운데"]
    print(f"\n[가운데 계좌 리플레이] {rp['창']}")
    for b in rp["산날들"]:
        print(f"  {b['산날']} 샀다(전날 최고점 대비 {b['전날낙폭'] * 100:.1f}%) — {b['첫달']}~{b['끝달']} 에 모은 {b['달수']}달치 · 그중 월급날 값이 더 쌌던 달 {b['월급날값이더쌌던달']}개")
    lf = rp["끝까지현금"]
    print(f"  끝까지 현금: {lf['첫달']}~{lf['끝달']} 에 모은 {lf['달수']}달치")
    print(f"  산 달 {rp['산달']}개 중 월급날보다 비싸게 산 달 {rp['월급날보다비싸게산달']}개")
    s = out["반론_짧게_10_12개월"]
    print()
    line("반론 · 10% 떨어지면 사기 · 최대 12개월", s)
    f = out["반론_50가지"]
    t = f["가장많이이긴가지"]
    print(f"\n[반론 · 기다리는 방법 {f['가지수']}가지] 382개 계좌 중 절반 넘게 이긴 방법 {f['절반넘게이긴가지']}가지 · "
          f"가장 많이 이긴 방법 {t['기다림이더남음']}번(−{t['X']}% · 최대 {t['K']}개월 · {'절반' if t['절반'] else '전부'})")
    for c in f["cells"]:
        print(f"  −{c['X']:>2}% · 최대 {str(c['K']):>3}개월 · {'절반' if c['절반'] else '전부'}: 기다리기가 더 남은 {c['기다림이더남음']:>3}/{c['창수']} · 가운데 비율 {c['중앙비율']:.4f}")
    print()
    for yrs in (5, 15, 20):
        line(f"반론 · 기간을 {yrs}년으로 · 20% 떨어지면 사기", out[f"반론_기간_{yrs}년"], yrs)
    print("\n[한계 · 수수료·세금 어림] VFINX 원래 종가 + 배당 기록으로 다시 지은 총수익 · 10년짜리 계좌")
    for k, v in out["한계_비용"].items():
        print(f"  {k}: 월급날 바로 {v['바로가더남음']}번 · 기다리기 {v['기다림이더남음']}번 (계좌 {v['창수']}개)")
    print("  (수수료 = 산 몫의 0.25% · 이자세 = 현금 이자의 15.4% · 배당세 = 배당의 15%)")
    print("\n※ 야후 069500.KS 는 2007-02-07 다음 봉이 2009-04-17 이다(2008년 전체 공백) — 코스피200 ETF 는 2009-05 부터만 쓴다")
    line("한계 · 코스피200 ETF(KODEX200) · 2009-05~ · 원화 · 현금 이자 0%", out["한계_KODEX200_이자0"])
    pk = out["왜_비싸게산달_KODEX200"]
    print(f"  기다렸다 산 달 {pk['산달']}개 중 월급날 값보다 싸게 산 달 {pk['월급날보다싸게산달']}개 · 비싸게 산 달 {pk['월급날보다비싸게산달']}개 · 같은 날 산 달 {pk['같은날산달']}개")
    line("한계 · 코스피200 ETF(KODEX200) · 2009-05~ · 원화 · 현금 이자 3%", out["한계_KODEX200_이자3"])
    line("한계 · 코스피200 ETF(TIGER200) · 2009-05~ · 원화 · 현금 이자 0%", out["한계_TIGER200_이자0"])
    line("한계 · 코스피 지수(가격지수 · 배당 0%) · 1997~ · 현금 이자 0%", out["한계_코스피지수_배당0_이자0"])
    line("한계 · 코스피 지수(가격지수 · 배당 0%) · 1997~ · 현금 이자 3%", out["한계_코스피지수_배당0_이자3"])
    line("한계 · 코스피 지수 + 배당 연 1.8%(어림) · 1997~ · 현금 이자 0%", out["한계_코스피지수_배당18_이자0"])
    line("한계 · 코스피 지수 + 배당 연 1.8%(어림) · 1997~ · 현금 이자 3%", out["한계_코스피지수_배당18_이자3"])
    line("한계 · S&P500 원화로(SPY × 원달러) · 현금 이자 0%", out["한계_SP500원화_이자0"])
    line("한계 · S&P500 원화로(SPY × 원달러) · 현금 이자 3%", out["한계_SP500원화_이자3"])
    line("한계 · 나스닥100(QQQ) · 미국 단기 국채 이자", out["한계_나스닥100"])
    print("\n끝.")


if __name__ == "__main__":
    main()

알고랩 Strategy Lab — 전략을 검증하고, 검증한 것만 자동매매로 옮깁니다. 무서운 뉴스가 나온 날에도 정해 둔 날에 사는 일은 사람보다 봇이 잘합니다. 알고랩 자동매매 살펴보기 · 전략 검증 전체 보기

과거 데이터를 돌려 본 결과이며 미래 수익을 보장하지 않습니다. 투자 판단과 책임은 본인에게 있습니다.