glorifiedjx commited on
Commit
fd48404
ยท
verified ยท
1 Parent(s): 124168e

upto date!

Browse files
Files changed (1) hide show
  1. vcp_debug.py +184 -126
vcp_debug.py CHANGED
@@ -1,24 +1,22 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- VCP ๋ŒํŒŒ ๊ฐ์ง€ - ๋„ค ์ •์˜ ๋ฒ„์ „ ('์กฐ์šฉํ•œ N์ผ -> ๊ฑฐ๋ž˜๋Ÿ‰+๊ฐ€๊ฒฉ ํญ๋ฐœ')
4
  TICKER ํ•˜๋‚˜๋งŒ ๋ฐ”๊พธ๋ฉด ๋จ!
5
 
6
- [ ๋ŒํŒŒ ์กฐ๊ฑด (๋ชจ๋‘ ์ถฉ์กฑ) ]
7
- 1. ํญ๋ฐœ์ผ ๊ฑฐ๋ž˜๋Ÿ‰ >= ์ง์ „ CONTRACT_DAYS ์ผ ๊ฑฐ๋ž˜๋Ÿ‰ ์ค‘ ์ตœ๋Œ€ ร— VOL_DRYUP_SURGE
8
- (= ์กฐ์šฉํ–ˆ๋‹ค๊ฐ€ ๊ฑฐ๋ž˜๋Ÿ‰์ด ํ™• ํ„ฐ์ง. '๊ฑฐ๋ž˜๋Ÿ‰ 20์ผ์„ '์ด ์•„๋‹ˆ๋ผ '์ง์ „ ์กฐ์šฉํ•œ ๋‚  ๋Œ€๋น„'๋ผ์„œ
9
- ํ‰์†Œ ๊ฑฐ๋ž˜๋Ÿ‰์ด ์ ์€ ์ข…๋ชฉ/๊ตฌ๊ฐ„์—์„œ๋„ ์žกํžˆ๊ณ , ํญ๋ฐœ ํ›„ ํ‰๊ท ์ด ํŠ€๋Š” ๋ฌธ์ œ๋„ ์—†์Œ.)
10
- 2. ํญ๋ฐœ์ผ ์ข…๊ฐ€ > ์ง์ „ BASE_LOOKBACK์ผ ๊ณ ๊ฐ€ (๊ฐ€๊ฒฉ ๋ŒํŒŒ)
11
- 3. ํญ๋ฐœ์ผ ์ƒ์Šน๋ฅ (์ „์ผ ์ข…๊ฐ€ ๋Œ€๋น„) >= MIN_PRICE_GAIN
12
- 4. ํญ๋ฐœ์ผ ์–‘๋ด‰ (์ข…๊ฐ€ > ์‹œ๊ฐ€)
13
-
14
- โ˜… ์ด ํŒŒ์ผ์˜ '๊ฐ์ง€ ์„ค์ •' ๋ธ”๋ก์€ vcp_sell_debug.py ์™€ ๋˜‘๊ฐ™์ด ์œ ์ง€ํ•  ๊ฒƒ!
15
  """
16
 
17
  import sys, ssl
18
  import pandas as pd
19
  import numpy as np
20
  import FinanceDataReader as fdr
21
- from datetime import datetime, timedelta
 
 
22
 
23
  ssl._create_default_https_context = ssl._create_unverified_context
24
  if sys.platform == 'win32':
@@ -27,137 +25,197 @@ if sys.platform == 'win32':
27
  # =============================================
28
  # [์—ฌ๊ธฐ๋งŒ ๋ฐ”๊พธ๋ฉด ๋จ]
29
  # =============================================
30
- TICKER = '007340'
31
 
32
  # =============================================
33
- # ๊ฐ์ง€ ์„ค์ • (โ˜… vcp_sell_debug.py ์™€ ๋˜‘๊ฐ™์ด!)
34
  # =============================================
35
- CONTRACT_DAYS = 2 # ํญ๋ฐœ ์ง์ „ '์กฐ์šฉํ•œ ๋‚ ' ์ˆ˜
36
- # VOL_DRYUP_SURGE = 6.0 # ํญ๋ฐœ์ผ ๊ฑฐ๋ž˜๋Ÿ‰ รท ์ง์ „ ์กฐ์šฉํ•œ ๋‚  ํ‰๊ท  (์กฐ์šฉํ–ˆ๋‹ค๊ฐ€ ๋ช‡ ๋ฐฐ๋กœ ํ„ฐ์กŒ๋‚˜)
37
- VOL_DRYUP_SURGE = 5.0 # ํญ๋ฐœ์ผ ๊ฑฐ๋ž˜๋Ÿ‰ รท ์ง์ „ ์กฐ์šฉํ•œ ๋‚  ํ‰๊ท  (์กฐ์šฉํ–ˆ๋‹ค๊ฐ€ ๋ช‡ ๋ฐฐ๋กœ ํ„ฐ์กŒ๋‚˜)
38
- MIN_PRICE_GAIN = 0.13 # ํญ๋ฐœ์ผ ์ƒ์Šน๋ฅ  (์ „์ผ ์ข…๊ฐ€ ๋Œ€๋น„)
39
- BASE_LOOKBACK = 10 # ํญ๋ฐœ์ผ ์ข…๊ฐ€๊ฐ€ '์ง์ „ N์ผ ๊ณ ๊ฐ€'๋ฅผ ๋„˜์–ด์•ผ (๊ฐ€๊ฒฉ ๋ŒํŒŒ ๊ธฐ์ค€)
40
-
41
- # DIAGNOSE_DATE = '2025-12-16' # ์˜ˆ: '2025-07-08' -> ๊ทธ ๋‚ ์ด ์™œ ์žกํ˜”๋‚˜/์•ˆ ์žกํ˜”๋‚˜ ์ง์ ‘ ํŒ์ •
42
- DIAGNOSE_DATE = None
 
43
 
44
  # =============================================
45
- # ๋ถ„์„ ๊ธฐ๊ฐ„ (๋” ๊ณผ๊ฑฐ๊นŒ์ง€ ๋ณด๋ ค๋ฉด ANALYZE_DAYS๋งŒ)
46
  # =============================================
47
- ANALYZE_DAYS = 365 # ๋ฉฐ์น  ์ „๊นŒ์ง€์˜ ๋ŒํŒŒ๋ฅผ ๋ณผ์ง€ (730=2๋…„, 1095=3๋…„)
48
- WARMUP_DAYS = 40 # ๊ณ ๊ฐ€ lookback ์›Œ๋ฐ์—…์šฉ ์ถ”๊ฐ€ ๋กœ๋“œ (๋ณดํ†ต ๊ทธ๋Œ€๋กœ)
49
- DATA_START = (datetime.today() - timedelta(days=ANALYZE_DAYS + WARMUP_DAYS)).strftime('%Y-%m-%d')
50
- ANALYZE_FROM = (datetime.today() - timedelta(days=ANALYZE_DAYS)).strftime('%Y-%m-%d')
51
- ANALYZE_TO = datetime.today().strftime('%Y-%m-%d')
52
 
 
 
 
53
 
54
- # =============================================
55
- # ๋ŒํŒŒ ๊ฐ์ง€ (โ˜… vcp_sell_debug.py ์™€ ๋™์ผ ๋กœ์ง)
56
- # =============================================
57
- def find_vcp_breakouts(df):
58
- O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
59
- L = df['Low'].values.astype(float); C = df['Close'].values.astype(float)
60
- V = df['Volume'].values.astype(float)
61
- out = []
62
- for t in range(BASE_LOOKBACK, len(df)):
63
- quiet_max = V[t - CONTRACT_DAYS:t].max() # ์ง์ „ ์กฐ์šฉํ•œ ๋‚ ๋“ค ์ค‘ '์ตœ๋Œ€' ๊ฑฐ๋ž˜๋Ÿ‰
64
- if quiet_max <= 0 or C[t - 1] <= 0:
65
- continue
66
- surge = V[t] / quiet_max # ์ง์ „ ๋ฉฐ์น  '๊ฐ๊ฐ'์˜ N๋ฐฐ (ํ•˜๋‚˜๋ผ๋„ ํฌ๋ฉด ํƒˆ๋ฝ = ํญ๋ฐœ ๋‹ค์Œ๋‚  ๋ฐฉ์ง€)
67
- base_high = H[t - BASE_LOOKBACK:t].max()
68
- gain = C[t] / C[t - 1] - 1
69
- if (surge >= VOL_DRYUP_SURGE and C[t] > base_high
70
- and gain >= MIN_PRICE_GAIN and C[t] > O[t]):
71
- out.append({
72
- 'i': t, 'date': str(df.index[t])[:10],
73
- 'close': C[t], 'open': O[t], 'low': L[t],
74
- 'gain': round(gain * 100, 1), 'surge': round(surge, 2),
75
- 'base_high': base_high, 'base_low': float(L[t - BASE_LOOKBACK:t].min()),
76
- })
77
- return [b for b in out if b['date'] >= ANALYZE_FROM]
78
 
 
79
 
80
  # =============================================
81
- # ์ง„๋‹จ: ํŠน์ • ๋‚ ์งœ๊ฐ€ ์™œ ์žกํ˜”๋‚˜/์•ˆ ์žกํ˜”๋‚˜
82
  # =============================================
83
- def run_diagnose(df, date_str):
84
- O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
85
- C = df['Close'].values.astype(float); V = df['Volume'].values.astype(float)
86
- idx = [str(d)[:10] for d in df.index]
87
- pos = next((k for k, d in enumerate(idx) if d >= date_str), len(df) - 1)
88
- lo = max(BASE_LOOKBACK, pos - 12); hi = min(len(df), pos + 4)
89
- bh_label = f"{BASE_LOOKBACK}์ผ๊ณ ๊ฐ€"
90
-
91
- print("\n" + "=" * 92)
92
- print(f"[์ง„๋‹จ] {date_str} ์ฃผ๋ณ€ ์ผ๋ณ„ (๋ฐฐ์ˆ˜ = ๊ทธ๋‚  ๊ฑฐ๋ž˜๋Ÿ‰ / ์ง์ „ {CONTRACT_DAYS}์ผ '์ตœ๋Œ€' ๊ฑฐ๋ž˜๋Ÿ‰)")
93
- print("=" * 92)
94
- print(f"{'๋‚ ์งœ':<11}{'์ข…๊ฐ€':>8}{'์‹œ๊ฐ€':>8}{'๊ฑฐ๋ž˜๋Ÿ‰':>12}{'์ง์ „์ตœ๋Œ€':>12}{'๋ฐฐ์ˆ˜':>7}"
95
- f"{'๋Œ€๋น„%':>7}{'์–‘๋ด‰':>5}{bh_label:>9}{'๊ณ ๊ฐ€๋ŒํŒŒ':>7}")
96
- print("-" * 92)
97
- for k in range(lo, hi):
98
- qa = V[k - CONTRACT_DAYS:k].max()
99
- sg = (V[k] / qa) if qa > 0 else 0
100
- bh = H[k - BASE_LOOKBACK:k].max()
101
- gn = (C[k] / C[k - 1] - 1) * 100 if C[k - 1] > 0 else 0
102
- bull = 'O' if C[k] > O[k] else ''
103
- pbk = 'O' if C[k] > bh else ''
104
- mark = ' <==' if idx[k] == date_str else ''
105
- print(f"{idx[k]:<11}{C[k]:>8,.0f}{O[k]:>8,.0f}{V[k]:>12,.0f}{qa:>12,.0f}{sg:>6.1f}x"
106
- f"{gn:>+6.1f}%{bull:>5}{bh:>9,.0f}{pbk:>7}{mark}")
107
-
108
- # ํŒ์ •
109
- print("\n [ํŒ์ •]")
110
- target = idx[pos]
111
- if target != date_str:
112
- print(f" (์ฐธ๊ณ : {date_str}์€ ๊ฑฐ๋ž˜์ผ ์•„๋‹˜ -> ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด {target} ๊ธฐ์ค€)")
113
- qa = V[pos - CONTRACT_DAYS:pos].max()
114
- sg = (V[pos] / qa) if qa > 0 else 0
115
- bh = H[pos - BASE_LOOKBACK:pos].max()
116
- gn = (C[pos] / C[pos - 1] - 1) * 100 if C[pos - 1] > 0 else 0
117
- c1 = sg >= VOL_DRYUP_SURGE; c2 = C[pos] > bh
118
- c3 = gn >= MIN_PRICE_GAIN * 100; c4 = C[pos] > O[pos]
119
- print(f" ๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ {sg:.1f}x >= {VOL_DRYUP_SURGE}? {'O' if c1 else 'X'}")
120
- print(f" ์ข…๊ฐ€ {C[pos]:,.0f} > {BASE_LOOKBACK}์ผ๊ณ ๊ฐ€ {bh:,.0f}? {'O' if c2 else 'X'}")
121
- print(f" ์ƒ์Šน๋ฅ  {gn:+.1f}% >= {MIN_PRICE_GAIN*100:.0f}%? {'O' if c3 else 'X'}")
122
- print(f" ์–‘๋ด‰ (์ข…๊ฐ€ {C[pos]:,.0f} > ์‹œ๊ฐ€ {O[pos]:,.0f})? {'O' if c4 else 'X'}")
123
- if c1 and c2 and c3 and c4:
124
- print(f" => 4์กฐ๊ฑด ํ†ต๊ณผ -> ์žกํ˜€์•ผ ํ•จ.")
125
- else:
126
- fails = [n for n, ok in [(f'๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ ๋ถ€์กฑ({sg:.1f}x)', c1), ('๊ฐ€๊ฒฉ์ด ๊ณ ๊ฐ€ ๋ชป ๋„˜์Œ', c2),
127
- (f'์ƒ์Šน๋ฅ  ๋ถ€์กฑ({gn:+.1f}%)', c3), ('์Œ๋ด‰', c4)] if not ok]
128
- print(f" => ์•ˆ ์žกํž˜. ์ด์œ : {', '.join(fails)}")
129
- if not c1:
130
- print(f" (VOL_DRYUP_SURGE๋ฅผ {sg:.1f} ๋ฐ‘์œผ๋กœ ๋‚ฎ์ถ”๋ฉด ์žกํ˜€)")
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  # =============================================
134
- # ๋ฐ์ดํ„ฐ ๋กœ๋“œ + ์ถœ๋ ฅ
135
  # =============================================
136
- print("=" * 70)
137
- print(f"VCP ๋ŒํŒŒ ๊ฐ์ง€: {TICKER} | ๊ธฐ๊ฐ„: {ANALYZE_FROM} ~ {ANALYZE_TO}")
138
- print(f"์กฐ๊ฑด: ์ง์ „{CONTRACT_DAYS}์ผ ๋Œ€๋น„ ๊ฑฐ๋ž˜๋Ÿ‰ {VOL_DRYUP_SURGE}๋ฐฐ+ & {BASE_LOOKBACK}์ผ๊ณ ๊ฐ€ ๋ŒํŒŒ "
139
- f"& ์ƒ์Šน +{MIN_PRICE_GAIN*100:.0f}%+ & ์–‘๋ด‰")
140
- print("=" * 70)
141
-
142
- df = fdr.DataReader(TICKER, DATA_START, ANALYZE_TO)
143
- if df is None or len(df) == 0:
144
- print("๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
145
- print(f"๋ฐ์ดํ„ฐ: {len(df)}์ผ ๋กœ๋“œ ์™„๋ฃŒ\n")
146
-
147
- bos = find_vcp_breakouts(df)
148
- print(f"๊ฐ์ง€๋œ VCP ๋ŒํŒŒ: {len(bos)}๊ฐœ\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
- for n, b in enumerate(bos, 1):
151
- print("=" * 70)
152
- print(f"VCP #{n} ๋ŒํŒŒ {b['date']} @ {b['close']:,.0f}์›")
153
- print(f" ์ƒ์Šน๋ฅ  +{b['gain']}% | ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ(์ง์ „{CONTRACT_DAYS}์ผ์˜ ์ตœ๋Œ€ ๊ธฐ์ค€) | "
154
- f"{BASE_LOOKBACK}์ผ๊ณ ๊ฐ€({b['base_high']:,.0f}) ๋ŒํŒŒ | ์–‘๋ด‰")
 
 
155
  print()
156
 
157
- print("=" * 70)
158
- print(f"์š”์•ฝ: ์ด {len(bos)}๊ฐœ ๋ŒํŒŒ")
159
- for b in bos:
160
- print(f" {b['date']} | {b['close']:,.0f}์› | +{b['gain']}% | ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ")
161
-
162
- if DIAGNOSE_DATE:
163
- run_diagnose(df, DIAGNOSE_DATE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ VCP (Volatility Contraction Pattern) - ๋ฏธ๋„ˆ๋น„๋‹ˆ ๊ธฐ์ค€
4
  TICKER ํ•˜๋‚˜๋งŒ ๋ฐ”๊พธ๋ฉด ๋จ!
5
 
6
+ ๋ŒํŒŒ ์กฐ๊ฑด (๋ชจ๋‘ ์ถฉ์กฑํ•ด์•ผ):
7
+ 1. ์ข…๊ฐ€๊ฐ€ ์ˆ˜์ถ•๊ตฌ๊ฐ„ ๊ณ ๊ฐ€๋ฅผ ๋ŒํŒŒ
8
+ 2. ๋‹น์ผ ๊ฐ€๊ฒฉ ์ƒ์Šน๋ฅ  3% ์ด์ƒ (์ „์ผ ์ข…๊ฐ€ ๋Œ€๋น„)
9
+ 3. ๋‹น์ผ ์–‘๋ด‰ (์ข…๊ฐ€ > ์‹œ๊ฐ€)
10
+ 4. ๊ฑฐ๋ž˜๋Ÿ‰ 20์ผMA ร— 2.0 ์ด์ƒ ํญ๋ฐœ
 
 
 
 
11
  """
12
 
13
  import sys, ssl
14
  import pandas as pd
15
  import numpy as np
16
  import FinanceDataReader as fdr
17
+ from datetime import datetime, timedelta, timezone
18
+ _KST = timezone(timedelta(hours=9))
19
+ def _now_kst(): return datetime.now(_KST)
20
 
21
  ssl._create_default_https_context = ssl._create_unverified_context
22
  if sys.platform == 'win32':
 
25
  # =============================================
26
  # [์—ฌ๊ธฐ๋งŒ ๋ฐ”๊พธ๋ฉด ๋จ]
27
  # =============================================
28
+ TICKER = '222080'
29
 
30
  # =============================================
31
+ # ์„ค์ •
32
  # =============================================
33
+ VOL_MA_PERIOD = 20
34
+ VOL_CONTRACT_THR = 0.8 # ์ˆ˜์ถ• ๊ธฐ์ค€ (20์ผMA ร— 0.8 ์ดํ•˜)
35
+ VOL_EXPLODE_THR = 2.0 # ํญ๋ฐœ ๊ธฐ์ค€ (20์ผMA ร— 2.0 ์ด์ƒ)
36
+ MIN_PRICE_GAIN = 0.03 # ๋ŒํŒŒ์ผ ์ตœ์†Œ ์ƒ์Šน๋ฅ  (3%)
37
+ MIN_CONTRACT_DAYS = 2 # ์ตœ์†Œ ์ˆ˜์ถ• ์ผ์ˆ˜
38
+ EXPLODE_WINDOW = 5 # ์ˆ˜์ถ• ํ›„ N์ผ ๋‚ด ๋ŒํŒŒ ํ™•์ธ
39
+ DATA_START = (_now_kst() - timedelta(days=400)).strftime('%Y-%m-%d')
40
+ ANALYZE_FROM = (_now_kst() - timedelta(days=365)).strftime('%Y-%m-%d')
41
+ ANALYZE_TO = _now_kst().strftime('%Y-%m-%d') # ์˜ค๋Š˜(KST) โ€” ๋งค ์‹คํ–‰์‹œ ์ตœ์‹ 
42
 
43
  # =============================================
44
+ # ๋ฐ์ดํ„ฐ ๋กœ๋“œ
45
  # =============================================
46
+ print("=" * 65)
47
+ print(f"VCP ๋ถ„์„: {TICKER} | ๊ธฐ๊ฐ„: {ANALYZE_FROM} ~ {ANALYZE_TO}")
48
+ print("=" * 65)
 
 
49
 
50
+ df = fdr.DataReader(TICKER, DATA_START, ANALYZE_TO)
51
+ if df is None or len(df) == 0:
52
+ print("๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
53
 
54
+ df['vol_ma20'] = df['Volume'].rolling(VOL_MA_PERIOD).mean()
55
+ df['vol_contract'] = df['Volume'] < df['vol_ma20'] * VOL_CONTRACT_THR
56
+ df['price_range'] = df['High'] - df['Low']
57
+ df['range_ma10'] = df['price_range'].rolling(10).mean()
58
+ df['prev_close'] = df['Close'].shift(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ print(f"๋ฐ์ดํ„ฐ: {len(df)}์ผ ๋กœ๋“œ ์™„๋ฃŒ\n")
61
 
62
  # =============================================
63
+ # VCP ๊ฐ์ง€
64
  # =============================================
65
+ vcps = []
66
+ i = VOL_MA_PERIOD
67
+
68
+ while i < len(df) - 1:
69
+ if not df['vol_contract'].iloc[i]:
70
+ i += 1; continue
71
+
72
+ # ์ˆ˜์ถ• ๊ตฌ๊ฐ„ ์ฐพ๊ธฐ
73
+ start = i
74
+ while start > VOL_MA_PERIOD and df['vol_contract'].iloc[start - 1]:
75
+ start -= 1
76
+ end = i
77
+ while end < len(df) - 1 and df['vol_contract'].iloc[end + 1]:
78
+ end += 1
79
+
80
+ days = end - start + 1
81
+ if days < MIN_CONTRACT_DAYS:
82
+ i = end + 1; continue
83
+
84
+ zone = df.iloc[start:end+1]
85
+ zone_low = float(zone['Low'].min())
86
+ zone_high = float(zone['High'].max())
87
+ zone_vol = float(zone['Volume'].mean())
88
+ vol_ma = float(df['vol_ma20'].iloc[end])
89
+ vol_ratio = round(zone_vol / vol_ma, 2) if vol_ma > 0 else 0
90
+
91
+ # ๊ฐ€๊ฒฉ ์ˆ˜์ถ• ํ™•์ธ
92
+ pre_range_ma = float(df['range_ma10'].iloc[max(0, start-1)])
93
+ zone_range = float(zone['price_range'].mean())
94
+ is_contracting = (zone_range < pre_range_ma) if pre_range_ma > 0 else False
95
+
96
+ # ๋ŒํŒŒ ํ™•์ธ (4๊ฐ€์ง€ ์กฐ๊ฑด ๋ชจ๋‘)
97
+ breakout = False
98
+ breakout_date = None
99
+ breakout_price = None
100
+ breakout_vol_r = None
101
+ breakout_gain = None
102
+ fail_reason = None
103
+
104
+ for j in range(end + 1, min(end + EXPLODE_WINDOW + 1, len(df))):
105
+ close_j = float(df['Close'].iloc[j])
106
+ open_j = float(df['Open'].iloc[j])
107
+ vol_j = float(df['Volume'].iloc[j])
108
+ vol_ma_j = float(df['vol_ma20'].iloc[j])
109
+ prev_close = float(df['prev_close'].iloc[j])
110
+
111
+ if prev_close == 0 or pd.isna(prev_close):
112
+ continue
113
 
114
+ price_gain = (close_j - prev_close) / prev_close # ์ „์ผ ๋Œ€๋น„ ์ƒ์Šน๋ฅ 
115
+ is_bullish = close_j > open_j # ์–‘๋ด‰
116
+ price_break = close_j > zone_high # ๊ตฌ๊ฐ„ ๊ณ ๊ฐ€ ๋ŒํŒŒ
117
+ vol_break = vol_j > vol_ma_j * VOL_EXPLODE_THR # ๊ฑฐ๋ž˜๋Ÿ‰ ํญ๋ฐœ
118
+ gain_ok = price_gain >= MIN_PRICE_GAIN # 3% ์ด์ƒ ์ƒ์Šน
119
+
120
+ if price_break and vol_break and gain_ok and is_bullish:
121
+ breakout = True
122
+ breakout_date = str(df.index[j])[:10]
123
+ breakout_price = close_j
124
+ breakout_vol_r = round(vol_j / vol_ma_j, 2)
125
+ breakout_gain = round(price_gain * 100, 1)
126
+ break
127
+ elif price_break:
128
+ # ๊ฐ€๊ฒฉ์€ ๋ŒํŒŒํ–ˆ์ง€๋งŒ ๋‹ค๋ฅธ ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ โ†’ ์ด์œ  ๊ธฐ๋ก
129
+ reasons = []
130
+ if not vol_break:
131
+ reasons.append(f"๊ฑฐ๋ž˜๋Ÿ‰ ๋ถ€์กฑ({round(vol_j/vol_ma_j,1)}x, ํ•„์š” {VOL_EXPLODE_THR}x)")
132
+ if not gain_ok:
133
+ reasons.append(f"์ƒ์Šน๋ฅ  ๋ถ€์กฑ({price_gain*100:+.1f}%, ํ•„์š” {MIN_PRICE_GAIN*100:.0f}%+)")
134
+ if not is_bullish:
135
+ reasons.append("์Œ๋ด‰")
136
+ fail_reason = str(df.index[j])[:10] + " ๊ฐ€๊ฒฉ๋ŒํŒŒํ–ˆ์ง€๋งŒ: " + ", ".join(reasons)
137
+
138
+ vcp = {
139
+ 'start_date': str(df.index[start])[:10],
140
+ 'end_date': str(df.index[end])[:10],
141
+ 'days': days,
142
+ 'zone_low': round(zone_low, 0),
143
+ 'zone_high': round(zone_high, 0),
144
+ 'vol_ratio': vol_ratio,
145
+ 'is_contracting': is_contracting,
146
+ 'zone_range': round(zone_range, 0),
147
+ 'pre_range_ma': round(pre_range_ma, 0),
148
+ 'confirmed': breakout,
149
+ 'breakout_date': breakout_date,
150
+ 'breakout_price': breakout_price,
151
+ 'breakout_vol_r': breakout_vol_r,
152
+ 'breakout_gain': breakout_gain,
153
+ 'fail_reason': fail_reason,
154
+ }
155
+ vcps.append(vcp)
156
+ i = end + 1
157
+
158
+ # ๋ถ„์„ ๊ธฐ๊ฐ„ ํ•„ํ„ฐ
159
+ vcps = [v for v in vcps if v['end_date'] >= ANALYZE_FROM]
160
 
161
  # =============================================
162
+ # ๊ฒฐ๊ณผ ์ถœ๋ ฅ
163
  # =============================================
164
+ print(f"๊ฐ์ง€๋œ VCP: {len(vcps)}๊ฐœ\n")
165
+
166
+ if len(vcps) == 0:
167
+ print("VCP ํŒจํ„ด ์—†์Œ"); sys.exit()
168
+
169
+ for idx, v in enumerate(vcps, 1):
170
+ status = "[ํ™•์ •] " if v['confirmed'] else "[๋ฏธํ™•์ •]"
171
+ print(f"{'='*65}")
172
+ print(f"VCP #{idx} {status} {v['start_date']} ~ {v['end_date']} ({v['days']}์ผ)")
173
+ print(f"{'='*65}")
174
+ print(f" ์ˆ˜์ถ•๊ตฌ๊ฐ„: {v['zone_low']:,.0f} ~ {v['zone_high']:,.0f}์›")
175
+ print(f" ๊ฑฐ๋ž˜๋Ÿ‰: 20์ผMA ๋Œ€๋น„ {v['vol_ratio']*100:.0f}% (๊ธฐ์ค€ {VOL_CONTRACT_THR*100:.0f}% ์ดํ•˜)")
176
+
177
+ if v['pre_range_ma'] > 0:
178
+ shrink = (1 - v['zone_range'] / v['pre_range_ma']) * 100
179
+ mark = 'O' if v['is_contracting'] else 'X'
180
+ print(f" ๊ฐ€๊ฒฉ์ˆ˜์ถ•: {mark} (๊ตฌ๊ฐ„ {v['zone_range']:,.0f} vs ์ง์ „MA {v['pre_range_ma']:,.0f}, {shrink:+.0f}%)")
181
+
182
+ if v['confirmed']:
183
+ print(f" ๋ŒํŒŒ: {v['breakout_date']} | "
184
+ f"์ข…๊ฐ€ {v['breakout_price']:,.0f}์› | "
185
+ f"์ƒ์Šน๋ฅ  +{v['breakout_gain']}% | "
186
+ f"๊ฑฐ๋ž˜๋Ÿ‰ MAร—{v['breakout_vol_r']} | ์–‘๋ด‰")
187
+ elif v['fail_reason']:
188
+ print(f" ๋ŒํŒŒ์‹œ๋„: {v['fail_reason']}")
189
+ else:
190
+ print(f" ๋ŒํŒŒ: ์—†์Œ")
191
 
192
+ print(f"\n [ํŒ๋‹จ ์ด์œ ]")
193
+ print(f" - ๊ฑฐ๋ž˜๋Ÿ‰ {v['days']}์ผ ์—ฐ์† 20์ผMAร—{VOL_CONTRACT_THR} ์ดํ•˜ ์ˆ˜์ถ•")
194
+ print(f" - ๊ฐ€๊ฒฉ์ˆ˜์ถ•: {'ํ™•์ธ' if v['is_contracting'] else '๋ฏธํ™•์ธ'}")
195
+ if v['confirmed']:
196
+ print(f" - ๊ตฌ๊ฐ„๊ณ ๊ฐ€({v['zone_high']:,.0f}) ๋ŒํŒŒ + ์ƒ์Šน๋ฅ  {v['breakout_gain']}% + ๊ฑฐ๋ž˜๋Ÿ‰ MAร—{v['breakout_vol_r']} + ์–‘๋ด‰ โ†’ ํ™•์ •")
197
+ else:
198
+ print(f" - 4๊ฐ€์ง€ ๋ŒํŒŒ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ (๊ฐ€๊ฒฉ๋ŒํŒŒ+3%์ƒ์Šน+๊ฑฐ๋ž˜๋Ÿ‰2x+์–‘๋ด‰)")
199
  print()
200
 
201
+ # ์š”์•ฝ
202
+ confirmed = [v for v in vcps if v['confirmed']]
203
+ pending = [v for v in vcps if not v['confirmed']]
204
+
205
+ print("=" * 65)
206
+ print("์ตœ์ข… ์š”์•ฝ")
207
+ print("=" * 65)
208
+ print(f"์ „์ฒด: {len(vcps)}๊ฐœ | ํ™•์ •: {len(confirmed)}๏ฟฝ๏ฟฝ | ๋ฏธํ™•์ •: {len(pending)}๊ฐœ")
209
+
210
+ if confirmed:
211
+ print("\n[ํ™•์ • VCP]")
212
+ for v in confirmed:
213
+ print(f" {v['start_date']}~{v['end_date']} | "
214
+ f"๊ตฌ๊ฐ„ {v['zone_low']:,.0f}~{v['zone_high']:,.0f} | "
215
+ f"๋ŒํŒŒ {v['breakout_date']} +{v['breakout_gain']}% MAร—{v['breakout_vol_r']}")
216
+
217
+ if pending:
218
+ print("\n[๋ฏธํ™•์ • VCP]")
219
+ for v in pending:
220
+ print(f" {v['start_date']}~{v['end_date']} | "
221
+ f"๊ตฌ๊ฐ„ {v['zone_low']:,.0f}~{v['zone_high']:,.0f}")